diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1a7e5d0c..d3c771d8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -89,7 +89,7 @@ jobs:
- name: Install dependencies
run: npm ci
- - name: Run tests
+ - name: Run JS tests via Turbo
run: npm run test:js
native-kotlin-test:
@@ -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'
diff --git a/.gitignore b/.gitignore
index de6d9e01..34294cb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,4 +49,4 @@ npm-debug.log
## Build
/build
-/packages/voltra/ios/.build/
+/packages/ios-client/ios/.build/
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 5dec186d..92bcba9f 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -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",
@@ -364,7 +368,7 @@
}
},
{
- "files": ["packages/voltra/src/**/*.{ts,tsx,js,jsx}", "packages/voltra/generator/**/*.{ts,tsx,js,jsx}"],
+ "files": ["packages/generator/generator/**/*.{ts,tsx,js,jsx}"],
"rules": {
"no-restricted-imports": [
"error",
diff --git a/.prettierignore b/.prettierignore
index ce02fc7f..2f4f7afa 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,6 +2,7 @@
/android/build
/example/android
/packages/*/build
+/packages/*/expo-plugin/build
/packages/*/.expo
/packages/*/android/build
/packages/*/ios/.build
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b3591164..51746758 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,23 +12,30 @@ 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
@@ -36,28 +43,24 @@ The Expo plugin in `packages/expo-plugin/src/` handles all Xcode project setup d
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:
@@ -68,13 +71,13 @@ 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 the private generator workspace:
```
-data/components.json
+packages/generator/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
@@ -82,14 +85,27 @@ This file defines all components, their parameters, types, and short names used
npm run generate
```
-This generates:
+This runs the generator script in `@use-voltra/generator` at `packages/generator/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**: `packages/generator` is a private tooling workspace, not a published runtime package. When adding new components or modifying props, always update `packages/generator/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
@@ -125,8 +141,8 @@ 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`
+- **TypeScript**: `packages/core/src/renderer/renderer.ts` → `VOLTRA_PAYLOAD_VERSION`
+- **Swift**: `packages/ios-client/ios/shared/VoltraPayloadMigrator.swift` → `currentVersion`
### When to increment the version
@@ -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)
@@ -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
diff --git a/README.md b/README.md
index b7692cad..381bece5 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
+ }
+ ]
+ }
+ ]
+ ]
}
}
```
@@ -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 = (
@@ -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.
@@ -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
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..def59bb6
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: ['module:@react-native/babel-preset'],
+ plugins: ['@babel/plugin-transform-export-namespace-from'],
+}
diff --git a/example/__tests__/ios/live-activity-snapshots.harness.tsx b/example/__tests__/ios/live-activity-snapshots.harness.tsx
index 39125880..5119540b 100644
--- a/example/__tests__/ios/live-activity-snapshots.harness.tsx
+++ b/example/__tests__/ios/live-activity-snapshots.harness.tsx
@@ -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,
diff --git a/example/__tests__/ios/widget-snapshots.harness.tsx b/example/__tests__/ios/widget-snapshots.harness.tsx
index 3abec09f..490051cb 100644
--- a/example/__tests__/ios/widget-snapshots.harness.tsx
+++ b/example/__tests__/ios/widget-snapshots.harness.tsx
@@ -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'
diff --git a/example/app.json b/example/app.json
index 67063cc1..752d545d 100644
--- a/example/app.json
+++ b/example/app.json
@@ -1,16 +1,17 @@
{
"expo": {
- "name": "Voltra Example",
+ "name": "Voltra",
"slug": "voltra",
"scheme": "voltra",
"version": "1.0.0",
"orientation": "portrait",
+ "backgroundColor": "#020617",
"icon": "./assets/voltra-icon.jpg",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/voltra-splash.jpg",
"resizeMode": "cover",
- "backgroundColor": "#8232FF"
+ "backgroundColor": "#020617"
},
"android": {
"package": "com.callstackincubator.voltraexample",
@@ -26,14 +27,11 @@
},
"plugins": [
[
- "voltra",
+ "@use-voltra/ios-client",
{
"groupIdentifier": "group.callstackincubator.voltraexample",
"keychainGroup": "$(AppIdentifierPrefix)group.callstackincubator.voltraexample",
"enablePushNotifications": true,
- "liveActivity": {
- "supplementalActivityFamilies": ["small"]
- },
"widgets": [
{
"id": "weather",
@@ -70,135 +68,144 @@
}
}
],
- "android": {
- "enableNotifications": true,
- "widgets": [
- {
- "id": "voltra",
- "displayName": {
- "pl": "Widget Voltra",
- "en": "Voltra Widget"
- },
- "description": {
- "pl": "Widget z logo Voltra",
- "en": "Voltra logo widget"
- },
- "minCellWidth": 2,
- "minCellHeight": 2,
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "initialStatePath": "./widgets/android/android-voltra-widget-initial.tsx",
- "previewImage": "./assets/voltra-icon.jpg"
- },
- {
- "id": "interactive_todos",
- "displayName": {
- "pl": "Interaktywna lista zadań",
- "en": "Interactive Todos Widget"
- },
- "description": {
- "pl": "Test interaktywnych widgetów z polami wyboru, przełącznikami i przyciskami",
- "en": "Testing interactive widgets with checkboxes, switches, and buttons"
- },
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "previewLayout": "./assets/widgets/todos-preview.xml"
- },
- {
- "id": "image_preloading",
- "displayName": {
- "pl": "Widget wstępnego ładowania obrazów",
- "en": "Image Preloading Widget"
- },
- "description": {
- "pl": "Test wstępnego ładowania obrazów na Androidzie",
- "en": "Test image preloading on Android"
- },
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen"
- },
- {
- "id": "image_fallback",
- "displayName": {
- "pl": "Widget zapasowego obrazu",
- "en": "Image Fallback Widget"
- },
- "description": {
- "pl": "Test zapasowego obrazu z kolorem tła ze stylów",
- "en": "Test image fallback with backgroundColor from styles"
- },
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "initialStatePath": "./widgets/android/android-image-fallback-initial.tsx"
- },
- {
- "id": "chart_widget",
- "displayName": {
- "pl": "Widget wykresu",
- "en": "Chart Widget"
- },
- "description": {
- "pl": "Test komponentu Chart",
- "en": "Test Chart component"
- },
- "targetCellWidth": 3,
- "targetCellHeight": 3,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "initialStatePath": "./widgets/android-chart-widget-initial.tsx"
- },
- {
- "id": "portfolio",
- "displayName": {
- "pl": "Widget portfela",
- "en": "Portfolio Widget"
- },
- "description": {
- "pl": "Pokazuje wyniki portfela z wykresem i aktualizacjami na żywo",
- "en": "Shows portfolio performance with a chart and live updates"
- },
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "initialStatePath": "./widgets/android/android-portfolio-initial.tsx",
- "serverUpdate": {
- "url": "http://10.0.2.2:3333",
- "intervalMinutes": 15,
- "refresh": true
- }
+ "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": {
+ "pl": "Widget Voltra",
+ "en": "Voltra Widget"
},
- {
- "id": "material_colors",
- "displayName": {
- "pl": "Widget kolorów Material",
- "en": "Material Colors Widget"
- },
- "description": {
- "pl": "Porównanie renderowania po stronie klienta i serwera z dynamicznymi kolorami Androida",
- "en": "Compare client-side and server-side rendering with Android dynamic colors"
- },
- "targetCellWidth": 2,
- "targetCellHeight": 2,
- "resizeMode": "horizontal|vertical",
- "widgetCategory": "home_screen",
- "initialStatePath": "./widgets/android/android-material-colors-initial.tsx",
- "serverUpdate": {
- "url": "http://10.0.2.2:3333",
- "intervalMinutes": 15,
- "refresh": true
- }
+ "description": {
+ "pl": "Widget z logo Voltra",
+ "en": "Voltra logo widget"
+ },
+ "minCellWidth": 2,
+ "minCellHeight": 2,
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android/android-voltra-widget-initial.tsx",
+ "previewImage": "./assets/voltra-icon.jpg"
+ },
+ {
+ "id": "interactive_todos",
+ "displayName": {
+ "pl": "Interaktywna lista zadań",
+ "en": "Interactive Todos Widget"
+ },
+ "description": {
+ "pl": "Test interaktywnych widgetów z polami wyboru, przełącznikami i przyciskami",
+ "en": "Testing interactive widgets with checkboxes, switches, and buttons"
+ },
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "previewLayout": "./assets/widgets/todos-preview.xml"
+ },
+ {
+ "id": "image_preloading",
+ "displayName": {
+ "pl": "Widget wstępnego ładowania obrazów",
+ "en": "Image Preloading Widget"
+ },
+ "description": {
+ "pl": "Test wstępnego ładowania obrazów na Androidzie",
+ "en": "Test image preloading on Android"
+ },
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen"
+ },
+ {
+ "id": "image_fallback",
+ "displayName": {
+ "pl": "Widget zapasowego obrazu",
+ "en": "Image Fallback Widget"
+ },
+ "description": {
+ "pl": "Test zapasowego obrazu z kolorem tła ze stylów",
+ "en": "Test image fallback with backgroundColor from styles"
+ },
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android/android-image-fallback-initial.tsx"
+ },
+ {
+ "id": "chart_widget",
+ "displayName": {
+ "pl": "Widget wykresu",
+ "en": "Chart Widget"
+ },
+ "description": {
+ "pl": "Test komponentu Chart",
+ "en": "Test Chart component"
+ },
+ "targetCellWidth": 3,
+ "targetCellHeight": 3,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android-chart-widget-initial.tsx"
+ },
+ {
+ "id": "portfolio",
+ "displayName": {
+ "pl": "Widget portfela",
+ "en": "Portfolio Widget"
+ },
+ "description": {
+ "pl": "Pokazuje wyniki portfela z wykresem i aktualizacjami na żywo",
+ "en": "Shows portfolio performance with a chart and live updates"
+ },
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android/android-portfolio-initial.tsx",
+ "serverUpdate": {
+ "url": "http://10.0.2.2:3333",
+ "intervalMinutes": 15,
+ "refresh": true
+ }
+ },
+ {
+ "id": "material_colors",
+ "displayName": {
+ "pl": "Widget kolorów Material",
+ "en": "Material Colors Widget"
+ },
+ "description": {
+ "pl": "Porównanie renderowania po stronie klienta i serwera z dynamicznymi kolorami Androida",
+ "en": "Compare client-side and server-side rendering with Android dynamic colors"
+ },
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android/android-material-colors-initial.tsx",
+ "serverUpdate": {
+ "url": "http://10.0.2.2:3333",
+ "intervalMinutes": 15,
+ "refresh": true
}
- ]
- },
+ }
+ ],
"fonts": [
"@expo-google-fonts/merriweather/400Regular/Merriweather_400Regular.ttf",
"@expo-google-fonts/merriweather/700Bold/Merriweather_700Bold.ttf",
diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx
index e9aa16de..82b3e4c2 100644
--- a/example/app/_layout.tsx
+++ b/example/app/_layout.tsx
@@ -1,7 +1,6 @@
import { Stack } from 'expo-router'
import { SafeAreaProvider } from 'react-native-safe-area-context'
-import { BackgroundWrapper } from '~/components/BackgroundWrapper'
import { useVoltraEvents } from '~/hooks/useVoltraEvents'
import { updateAndroidVoltraWidget } from '~/widgets/android/updateAndroidVoltraWidget'
@@ -9,7 +8,7 @@ updateAndroidVoltraWidget({ width: 300, height: 200 })
const STACK_SCREEN_OPTIONS = {
headerShown: false,
- contentStyle: { backgroundColor: 'transparent' },
+ contentStyle: { backgroundColor: '#020617' },
}
export const unstable_settings = {
@@ -21,10 +20,9 @@ export default function Layout() {
return (
- {children}}
- >
+
+
+
-
-
-
diff --git a/example/app/android-widgets.tsx b/example/app/android-widgets.tsx
index c33ebf7a..15cc08de 100644
--- a/example/app/android-widgets.tsx
+++ b/example/app/android-widgets.tsx
@@ -1,5 +1,5 @@
-import AndroidScreen from '~/screens/android/AndroidScreen'
+import { Redirect } from 'expo-router'
-export default function AndroidWidgetsIndex() {
- return
+export default function AndroidWidgetsRedirect() {
+ return
}
diff --git a/example/app/android/(tabs)/_layout.tsx b/example/app/android/(tabs)/_layout.tsx
new file mode 100644
index 00000000..8b69bfe4
--- /dev/null
+++ b/example/app/android/(tabs)/_layout.tsx
@@ -0,0 +1,39 @@
+import Ionicons from '@expo/vector-icons/Ionicons'
+import { Tabs } from 'expo-router'
+
+const TAB_BAR_COLORS = {
+ active: '#A78BFA',
+ inactive: '#94A3B8',
+ background: '#020617',
+ border: 'rgba(148, 163, 184, 0.18)',
+}
+
+export default function AndroidTabsLayout() {
+ return (
+ ({
+ headerShown: false,
+ tabBarActiveTintColor: TAB_BAR_COLORS.active,
+ tabBarInactiveTintColor: TAB_BAR_COLORS.inactive,
+ tabBarStyle: {
+ backgroundColor: TAB_BAR_COLORS.background,
+ borderTopColor: TAB_BAR_COLORS.border,
+ },
+ tabBarIcon: ({ color, size }) => {
+ const iconName =
+ route.name === 'activity'
+ ? 'notifications-outline'
+ : route.name === 'widgets'
+ ? 'grid-outline'
+ : 'ellipsis-horizontal-circle-outline'
+
+ return
+ },
+ })}
+ >
+
+
+
+
+ )
+}
diff --git a/example/app/android/(tabs)/activity.tsx b/example/app/android/(tabs)/activity.tsx
new file mode 100644
index 00000000..a6d8eb9e
--- /dev/null
+++ b/example/app/android/(tabs)/activity.tsx
@@ -0,0 +1,5 @@
+import ActivityScreen from '~/screens/android/tabs/ActivityScreen'
+
+export default function AndroidActivityTab() {
+ return
+}
diff --git a/example/app/android/(tabs)/others.tsx b/example/app/android/(tabs)/others.tsx
new file mode 100644
index 00000000..fd941b75
--- /dev/null
+++ b/example/app/android/(tabs)/others.tsx
@@ -0,0 +1,5 @@
+import OthersScreen from '~/screens/android/tabs/OthersScreen'
+
+export default function AndroidOthersTab() {
+ return
+}
diff --git a/example/app/android/(tabs)/widgets.tsx b/example/app/android/(tabs)/widgets.tsx
new file mode 100644
index 00000000..4e8b6292
--- /dev/null
+++ b/example/app/android/(tabs)/widgets.tsx
@@ -0,0 +1,5 @@
+import WidgetsScreen from '~/screens/android/tabs/WidgetsScreen'
+
+export default function AndroidWidgetsTab() {
+ return
+}
diff --git a/example/app/index.tsx b/example/app/index.tsx
index 6f8cf7a5..76922d94 100644
--- a/example/app/index.tsx
+++ b/example/app/index.tsx
@@ -2,6 +2,5 @@ import { Redirect } from 'expo-router'
import { Platform } from 'react-native'
export default function Index() {
- const href = Platform.OS === 'android' ? '/android-widgets' : '/live-activities'
- return
+ return
}
diff --git a/example/app/ios/(tabs)/_layout.tsx b/example/app/ios/(tabs)/_layout.tsx
new file mode 100644
index 00000000..d46164fb
--- /dev/null
+++ b/example/app/ios/(tabs)/_layout.tsx
@@ -0,0 +1,39 @@
+import Ionicons from '@expo/vector-icons/Ionicons'
+import { Tabs } from 'expo-router'
+
+const TAB_BAR_COLORS = {
+ active: '#A78BFA',
+ inactive: '#94A3B8',
+ background: '#020617',
+ border: 'rgba(148, 163, 184, 0.18)',
+}
+
+export default function IOSTabsLayout() {
+ return (
+ ({
+ headerShown: false,
+ tabBarActiveTintColor: TAB_BAR_COLORS.active,
+ tabBarInactiveTintColor: TAB_BAR_COLORS.inactive,
+ tabBarStyle: {
+ backgroundColor: TAB_BAR_COLORS.background,
+ borderTopColor: TAB_BAR_COLORS.border,
+ },
+ tabBarIcon: ({ color, size }) => {
+ const iconName =
+ route.name === 'activity'
+ ? 'radio-outline'
+ : route.name === 'widgets'
+ ? 'grid-outline'
+ : 'ellipsis-horizontal-circle-outline'
+
+ return
+ },
+ })}
+ >
+
+
+
+
+ )
+}
diff --git a/example/app/ios/(tabs)/activity.tsx b/example/app/ios/(tabs)/activity.tsx
new file mode 100644
index 00000000..82224e0e
--- /dev/null
+++ b/example/app/ios/(tabs)/activity.tsx
@@ -0,0 +1,5 @@
+import ActivityScreen from '~/screens/ios/tabs/ActivityScreen'
+
+export default function IOSActivityTab() {
+ return
+}
diff --git a/example/app/ios/(tabs)/others.tsx b/example/app/ios/(tabs)/others.tsx
new file mode 100644
index 00000000..8ef55083
--- /dev/null
+++ b/example/app/ios/(tabs)/others.tsx
@@ -0,0 +1,5 @@
+import OthersScreen from '~/screens/ios/tabs/OthersScreen'
+
+export default function IOSOthersTab() {
+ return
+}
diff --git a/example/app/ios/(tabs)/widgets.tsx b/example/app/ios/(tabs)/widgets.tsx
new file mode 100644
index 00000000..0e27eeb4
--- /dev/null
+++ b/example/app/ios/(tabs)/widgets.tsx
@@ -0,0 +1,5 @@
+import WidgetsScreen from '~/screens/ios/tabs/WidgetsScreen'
+
+export default function IOSWidgetsTab() {
+ return
+}
diff --git a/example/app/live-activities.tsx b/example/app/live-activities.tsx
index dd6baee8..abd2bbb3 100644
--- a/example/app/live-activities.tsx
+++ b/example/app/live-activities.tsx
@@ -1,5 +1,5 @@
-import LiveActivitiesScreen from '~/screens/live-activities/LiveActivitiesScreen'
+import { Redirect } from 'expo-router'
-export default function LiveActivitiesIndex() {
- return
+export default function LiveActivitiesRedirect() {
+ return
}
diff --git a/example/app/testing-grounds.tsx b/example/app/testing-grounds.tsx
index b280df8b..61b00ae0 100644
--- a/example/app/testing-grounds.tsx
+++ b/example/app/testing-grounds.tsx
@@ -1,5 +1,6 @@
-import TestingGroundsScreen from '~/screens/testing-grounds/TestingGroundsScreen'
+import { Redirect } from 'expo-router'
+import { Platform } from 'react-native'
-export default function TestingGroundsIndex() {
- return
+export default function TestingGroundsRedirect() {
+ return
}
diff --git a/example/components/ActiveWidgetsAndroidCard.tsx b/example/components/ActiveWidgetsAndroidCard.tsx
index ac844459..008ee462 100644
--- a/example/components/ActiveWidgetsAndroidCard.tsx
+++ b/example/components/ActiveWidgetsAndroidCard.tsx
@@ -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'
diff --git a/example/components/ActiveWidgetsIOSCard.tsx b/example/components/ActiveWidgetsIOSCard.tsx
index 268961e4..809cc6ec 100644
--- a/example/components/ActiveWidgetsIOSCard.tsx
+++ b/example/components/ActiveWidgetsIOSCard.tsx
@@ -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'
diff --git a/example/components/BackgroundWrapper.tsx b/example/components/BackgroundWrapper.tsx
deleted file mode 100644
index d9363f07..00000000
--- a/example/components/BackgroundWrapper.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { ReactNode } from 'react'
-import { Image, StyleSheet, useWindowDimensions, View } from 'react-native'
-import { SafeAreaView } from 'react-native-safe-area-context'
-
-export type BackgroundWrapperProps = {
- children: ReactNode
-}
-
-export const BackgroundWrapper = ({ children }: BackgroundWrapperProps) => {
- const { width, height } = useWindowDimensions()
-
- return (
-
-
- {children}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- safeArea: {
- flex: 1,
- },
- image: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- zIndex: -1,
- },
-})
diff --git a/example/components/Button.tsx b/example/components/Button.tsx
index 9ca779e5..d8b74db5 100644
--- a/example/components/Button.tsx
+++ b/example/components/Button.tsx
@@ -42,9 +42,6 @@ const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#8232FF',
},
- primaryButtonPressed: {
- backgroundColor: '#6B28E0', // Darker purple for pressed state
- },
primaryButtonText: {
color: '#FFFFFF',
},
@@ -65,9 +62,6 @@ const styles = StyleSheet.create({
color: '#E2E8F0',
fontWeight: '600',
},
- secondaryActionButton: {
- marginLeft: 12,
- },
buttonDisabled: {
opacity: 0.6,
},
@@ -78,13 +72,4 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '600',
},
- endAllButton: {
- marginTop: 12,
- borderWidth: 1,
- borderColor: 'rgba(239, 68, 68, 0.35)',
- backgroundColor: 'rgba(239, 68, 68, 0.12)',
- },
- endAllButtonText: {
- color: '#F87171',
- },
})
diff --git a/example/components/ExampleSectionCards.tsx b/example/components/ExampleSectionCards.tsx
new file mode 100644
index 00000000..1f3095fa
--- /dev/null
+++ b/example/components/ExampleSectionCards.tsx
@@ -0,0 +1,58 @@
+import { useRouter } from 'expo-router'
+import { StyleSheet, View } from 'react-native'
+
+import { Button } from './Button'
+import { Card } from './Card'
+
+export type ExampleSection = {
+ id: string
+ title: string
+ description: string
+ route:
+ | '/testing-grounds/weather'
+ | '/testing-grounds/timer'
+ | '/testing-grounds/styling'
+ | '/testing-grounds/positioning'
+ | '/testing-grounds/progress'
+ | '/testing-grounds/components'
+ | '/testing-grounds/flex-playground'
+ | '/testing-grounds/chart-playground'
+ | '/testing-grounds/gradient-playground'
+ | '/testing-grounds/image-preloading'
+ | '/testing-grounds/image-fallback'
+ | '/testing-grounds/widget-scheduling'
+ | '/testing-grounds/server-driven-widgets'
+ | '/testing-grounds/channel-updates'
+ | '/android-widgets/pin'
+ | '/android-widgets/image-preloading'
+ | '/android-widgets/image-fallback'
+ | '/android-widgets/preview'
+ | '/android-widgets/charts'
+ | '/android-widgets/server-driven'
+ | '/android-widgets/material-colors'
+ | '/android-widgets/custom-fonts'
+}
+
+export type ExampleSectionCardsProps = {
+ sections: ExampleSection[]
+}
+
+export function ExampleSectionCards({ sections }: ExampleSectionCardsProps) {
+ const router = useRouter()
+
+ return sections.map((section) => (
+
+ {section.title}
+ {section.description}
+
+
+
+ ))
+}
+
+const styles = StyleSheet.create({
+ buttonContainer: {
+ marginTop: 16,
+ },
+})
diff --git a/example/components/ScreenLayout.tsx b/example/components/ScreenLayout.tsx
new file mode 100644
index 00000000..02ccd5a5
--- /dev/null
+++ b/example/components/ScreenLayout.tsx
@@ -0,0 +1,59 @@
+import { ReactNode } from 'react'
+import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
+
+export type ScreenLayoutProps = {
+ title: string
+ description?: string
+ children?: ReactNode
+ contentContainerStyle?: StyleProp
+}
+
+export function ScreenLayout({ title, description, children, contentContainerStyle }: ScreenLayoutProps) {
+ const insets = useSafeAreaInsets()
+
+ return (
+
+
+
+ {title}
+ {description ? {description} : null}
+
+ {children}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#020617',
+ },
+ scrollView: {
+ flex: 1,
+ backgroundColor: '#020617',
+ },
+ content: {
+ paddingHorizontal: 20,
+ paddingTop: 24,
+ },
+ header: {
+ marginBottom: 8,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ },
+ description: {
+ marginTop: 8,
+ fontSize: 14,
+ lineHeight: 20,
+ color: '#CBD5F5',
+ },
+})
diff --git a/example/components/live-activities/BasicLiveActivityUI.tsx b/example/components/live-activities/BasicLiveActivityUI.tsx
index 097ec093..6f6b75d5 100644
--- a/example/components/live-activities/BasicLiveActivityUI.tsx
+++ b/example/components/live-activities/BasicLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export function BasicLiveActivityUI() {
return (
diff --git a/example/components/live-activities/CompassLiveActivityUI.tsx b/example/components/live-activities/CompassLiveActivityUI.tsx
index 3936719e..67eba0e8 100644
--- a/example/components/live-activities/CompassLiveActivityUI.tsx
+++ b/example/components/live-activities/CompassLiveActivityUI.tsx
@@ -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) */
diff --git a/example/components/live-activities/DeepLinksLiveActivityUI.tsx b/example/components/live-activities/DeepLinksLiveActivityUI.tsx
index 563356b6..ddcf45b7 100644
--- a/example/components/live-activities/DeepLinksLiveActivityUI.tsx
+++ b/example/components/live-activities/DeepLinksLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export function DeepLinksLiveActivityUI() {
return (
diff --git a/example/components/live-activities/FlightLiveActivityUI.tsx b/example/components/live-activities/FlightLiveActivityUI.tsx
index fbb67c57..ba946d00 100644
--- a/example/components/live-activities/FlightLiveActivityUI.tsx
+++ b/example/components/live-activities/FlightLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export function FlightLiveActivityLockScreen() {
return (
diff --git a/example/components/live-activities/LiquidGlassLiveActivityUI.tsx b/example/components/live-activities/LiquidGlassLiveActivityUI.tsx
index d26d4358..85592b16 100644
--- a/example/components/live-activities/LiquidGlassLiveActivityUI.tsx
+++ b/example/components/live-activities/LiquidGlassLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export const VoltraLovesLiveActivity = () => {
return (
diff --git a/example/components/live-activities/MusicPlayerLiveActivityUI.tsx b/example/components/live-activities/MusicPlayerLiveActivityUI.tsx
index 55544a12..ff976c3a 100644
--- a/example/components/live-activities/MusicPlayerLiveActivityUI.tsx
+++ b/example/components/live-activities/MusicPlayerLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export type Song = {
title: string
diff --git a/example/components/live-activities/SupplementalFamiliesUI.tsx b/example/components/live-activities/SupplementalFamiliesUI.tsx
index 9b59bb77..57f2821c 100644
--- a/example/components/live-activities/SupplementalFamiliesUI.tsx
+++ b/example/components/live-activities/SupplementalFamiliesUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
export function SupplementalFamiliesLockScreen() {
return (
diff --git a/example/components/live-activities/WorkoutLiveActivityUI.tsx b/example/components/live-activities/WorkoutLiveActivityUI.tsx
index 1c49b872..163f04d7 100644
--- a/example/components/live-activities/WorkoutLiveActivityUI.tsx
+++ b/example/components/live-activities/WorkoutLiveActivityUI.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
type WorkoutLiveActivityUIProps = {
heartRate: number
diff --git a/example/hooks/useVoltraEvents.ts b/example/hooks/useVoltraEvents.ts
index 286886ce..225371fe 100644
--- a/example/hooks/useVoltraEvents.ts
+++ b/example/hooks/useVoltraEvents.ts
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { Platform } from 'react-native'
-import { addVoltraListener } from 'voltra/client'
+import { addVoltraListener } from '@use-voltra/ios-client'
export const useVoltraEvents = (): void => {
useEffect(() => {
diff --git a/example/notifications/voltraAndroidOngoingNotificationBackground.ts b/example/notifications/voltraAndroidOngoingNotificationBackground.ts
index 0dd68167..f5fb0697 100644
--- a/example/notifications/voltraAndroidOngoingNotificationBackground.ts
+++ b/example/notifications/voltraAndroidOngoingNotificationBackground.ts
@@ -1,8 +1,8 @@
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
-import type { AndroidOngoingNotificationPayload } from 'voltra/android'
-import type { StartAndroidOngoingNotificationOptions } from 'voltra/android/client'
-import { stopAndroidOngoingNotification, upsertAndroidOngoingNotification } from 'voltra/android/client'
+import type { AndroidOngoingNotificationPayload } from '@use-voltra/android'
+import type { StartAndroidOngoingNotificationOptions } from '@use-voltra/android-client'
+import { stopAndroidOngoingNotification, upsertAndroidOngoingNotification } from '@use-voltra/android-client'
export const VOLTRA_BACKGROUND_NOTIFICATION_TASK = 'voltra-background-notification-task'
const DEFAULT_CHANNEL_ID = 'voltra_live_updates'
diff --git a/example/package.json b/example/package.json
index 60cd89b4..4f5999c6 100644
--- a/example/package.json
+++ b/example/package.json
@@ -51,7 +51,13 @@
"react-native-web": "^0.21.0",
"react-native-webview": "13.16.0",
"react-native-worklets": "~0.7.0",
- "voltra": "*"
+ "@use-voltra/ios": "*",
+ "@use-voltra/ios-client": "*",
+ "@use-voltra/android": "*",
+ "@use-voltra/android-client": "*",
+ "@use-voltra/ios-server": "*",
+ "@use-voltra/android-server": "*",
+ "@use-voltra/server": "*"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/example/screens/android/AndroidChartScreen.tsx b/example/screens/android/AndroidChartScreen.tsx
index f1d06a04..63a19f76 100644
--- a/example/screens/android/AndroidChartScreen.tsx
+++ b/example/screens/android/AndroidChartScreen.tsx
@@ -1,10 +1,11 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { AndroidWidgetFamily, VoltraWidgetPreview } from 'voltra/android/client'
+import { AndroidWidgetFamily, VoltraWidgetPreview } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { AreaChartWidget, BarChartWidget, LineChartWidget, PieChartWidget } from '~/widgets/AndroidChartWidget'
type ChartType = 'bar' | 'line' | 'area' | 'pie'
@@ -42,90 +43,63 @@ export default function AndroidChartScreen() {
const [selectedSize, setSelectedSize] = useState('large')
return (
-
-
- Chart Widgets
-
- Preview Android chart widgets rendered via Canvas bitmap. Charts are drawn natively using
- android.graphics.Canvas and displayed as a Glance Image.
-
-
-
- Chart Type
- {CHART_TYPES.find((c) => c.id === selectedChart)?.description}
-
-
- {CHART_TYPES.map((chart) => (
-
-
-
-
-
- Widget Size
+
+
+ Chart Type
+ {CHART_TYPES.find((c) => c.id === selectedChart)?.description}
+
- {WIDGET_SIZES.map((size) => (
+ {CHART_TYPES.map((chart) => (
-
+
+
-
- Live Preview
- Rendered using the native Glance renderer with Canvas-based chart bitmap.
-
-
-
-
-
-
-
-
+
+ Widget Size
+
+ {WIDGET_SIZES.map((size) => (
+
+
-
-
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 24,
- },
scrollArea: {
marginHorizontal: -4,
},
diff --git a/example/screens/android/AndroidCustomFontScreen.tsx b/example/screens/android/AndroidCustomFontScreen.tsx
index 589ff9a5..8abbb867 100644
--- a/example/screens/android/AndroidCustomFontScreen.tsx
+++ b/example/screens/android/AndroidCustomFontScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { VoltraAndroid } from 'voltra/android'
-import { AndroidWidgetFamily, VoltraWidgetPreview } from 'voltra/android/client'
+import { StyleSheet, Text, View } from 'react-native'
+import { VoltraAndroid } from '@use-voltra/android'
+import { AndroidWidgetFamily, VoltraWidgetPreview } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
const WIDGET_SIZES: { id: AndroidWidgetFamily; title: string }[] = [
{ id: 'mediumWide', title: 'Medium Wide' },
@@ -79,95 +80,73 @@ export default function AndroidCustomFontScreen() {
const [selectedSize, setSelectedSize] = useState('large')
return (
-
-
- Custom Font Widgets
-
- Demonstrates custom font rendering on Android Glance widgets.{'\n'}
- Text with renderAsBitmap is drawn to a Canvas bitmap with a custom Typeface
- loaded from assets/fonts/.
-
+
+
+ Demonstrates custom font rendering on Android Glance widgets.{`\n`}
+ Text with renderAsBitmap is drawn to a Canvas bitmap with a custom Typeface
+ loaded from assets/fonts/.
+
-
- Widget Size
-
- {WIDGET_SIZES.map((size) => (
-
-
+
+ Widget Size
+
+ {WIDGET_SIZES.map((size) => (
+
+
-
- Live Preview
-
- Pacifico (script) and Press Start 2P (pixel) are rendered as bitmaps. Compare with the native Glance text
- below them.
-
-
-
-
-
-
-
+
+ Live Preview
+
+ Pacifico (script) and Press Start 2P (pixel) are rendered as bitmaps. Compare with the native Glance text
+ below them.
+
+
+
+
+
+
-
+
+
-
- How It Works
-
- 1. Add font packages (e.g. @expo-google-fonts/pacifico) or place .ttf files in your project{'\n'}
- 2. Add font paths to the "fonts" array in your Voltra plugin config{'\n'}
- 3. Set fontFamily in style to the filename without extension{'\n'}
- 4. Add renderAsBitmap prop to {'<'}Text{'>'}
- {'\n'}
- 5. Run prebuild — the plugin copies fonts to assets/fonts/ automatically
-
-
+
+ How It Works
+
+ 1. Add font packages (e.g. @expo-google-fonts/pacifico) or place .ttf files in your project{`\n`}
+ 2. Add font paths to the "fonts" array in your Voltra plugin config{`\n`}
+ 3. Set fontFamily in style to the filename without extension{`\n`}
+ 4. Add renderAsBitmap prop to {'<'}Text{'>'}
+ {`\n`}
+ 5. Run prebuild — the plugin copies fonts to assets/fonts/ automatically
+
+
-
-
-
-
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
+ description: {
fontSize: 14,
lineHeight: 20,
color: '#CBD5F5',
- marginBottom: 24,
+ marginBottom: 16,
},
code: {
fontFamily: 'monospace',
color: '#A78BFA',
},
- scrollArea: {
- marginHorizontal: -4,
- },
buttonRow: {
flexDirection: 'row',
flexWrap: 'wrap',
diff --git a/example/screens/android/AndroidImageFallbackScreen.tsx b/example/screens/android/AndroidImageFallbackScreen.tsx
index 413f9ed7..0b57f1f2 100644
--- a/example/screens/android/AndroidImageFallbackScreen.tsx
+++ b/example/screens/android/AndroidImageFallbackScreen.tsx
@@ -1,10 +1,11 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
-import { requestPinAndroidWidget, updateAndroidWidget } from 'voltra/android/client'
+import { Alert, Platform, StyleSheet, Text, View } from 'react-native'
+import { requestPinAndroidWidget, updateAndroidWidget } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { AndroidImageFallbackWidget } from '~/widgets/android/AndroidImageFallbackWidget'
const WIDGET_ID = 'image_fallback'
@@ -104,97 +105,71 @@ export default function AndroidImageFallbackScreen() {
}
return (
-
-
- Image Fallback Widget (Android)
-
- Test the new image fallback behavior on Android widgets. Pin the widget to your home screen, then use the
- buttons below to switch between different examples.
-
+
+
+ 1. Pin Widget to Home Screen
+
+ First, pin the widget to your home screen. You can then use the buttons below to update it with different
+ examples.
+
+
+
+
+
+
+
+ 2. Select Example to Display
+
+ Choose an example to display in the widget. The widget will update immediately on your home screen.
+
-
- 1. Pin Widget to Home Screen
-
- First, pin the widget to your home screen. You can then use the buttons below to update it with different
- examples.
-
-
+ {EXAMPLES.map((example) => (
+
-
-
-
- 2. Select Example to Display
-
- Choose an example to display in the widget. The widget will update immediately on your home screen.
-
-
- {EXAMPLES.map((example) => (
-
-
- ))}
-
-
-
- Migration Note
-
- The fallbackColor prop has been removed from the Image component.
-
-
- Before: fallbackColor="#E0E0E0"
-
-
- After: style={'{{ backgroundColor: "#E0E0E0" }}'}
-
-
- All style properties (backgroundColor, borderRadius, borders, etc.) now apply consistently to image
- fallbacks on both iOS and Android.
-
-
+ ))}
+
-
-
-
-
+
+ Migration Note
+
+ The fallbackColor prop has been removed from the Image component.
+
+
+ Before: fallbackColor="#E0E0E0"
+
+
+ After: style={'{{ backgroundColor: "#E0E0E0" }}'}
+
+
+ All style properties (backgroundColor, borderRadius, borders, etc.) now apply consistently to image fallbacks
+ on both iOS and Android.
+
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
buttonContainer: {
marginTop: 16,
},
diff --git a/example/screens/android/AndroidImagePreloadingScreen.tsx b/example/screens/android/AndroidImagePreloadingScreen.tsx
index ec7152fc..4cf19d28 100644
--- a/example/screens/android/AndroidImagePreloadingScreen.tsx
+++ b/example/screens/android/AndroidImagePreloadingScreen.tsx
@@ -1,10 +1,11 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native'
-import { VoltraAndroid } from 'voltra'
-import { clearPreloadedImages, preloadImages, reloadWidgets, updateAndroidWidget } from 'voltra/android/client'
+import { Alert, StyleSheet, Text, View } from 'react-native'
+import { VoltraAndroid } from '@use-voltra/android'
+import { clearPreloadedImages, preloadImages, reloadWidgets, updateAndroidWidget } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { TextInput } from '~/components/TextInput'
function generateRandomUrl(): string {
@@ -129,73 +130,46 @@ export default function AndroidImagePreloadingScreen() {
}
return (
-
-
- Android Image Preloading
-
- Test preloading images for Android widgets. Enter a URL to preload an image and update the widget, or
- overwrite existing preloaded images.
-
-
-
- Image URL
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Image URL
+
+
+
+
+
+
+
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#0B0F1A',
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
inputContainer: {
marginTop: 16,
},
diff --git a/example/screens/android/AndroidMaterialColorsScreen.tsx b/example/screens/android/AndroidMaterialColorsScreen.tsx
index dd254487..dfe28d2d 100644
--- a/example/screens/android/AndroidMaterialColorsScreen.tsx
+++ b/example/screens/android/AndroidMaterialColorsScreen.tsx
@@ -1,16 +1,17 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
+import { Alert, Platform, StyleSheet, Text, View } from 'react-native'
import {
reloadAndroidWidgets,
requestPinAndroidWidget,
setWidgetServerCredentials,
updateAndroidWidget,
VoltraWidgetPreview,
-} from 'voltra/android/client'
+} from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import {
AndroidMaterialColorsWidget,
type AndroidMaterialColorsRenderSource,
@@ -127,108 +128,82 @@ export default function AndroidMaterialColorsScreen() {
}
return (
-
-
- Material Colors Widget
-
- Test the same Android widget through both render paths. It uses Android semantic color tokens, so both
- client-side and server-side rendering resolve native Material You colors directly inside Glance.
-
-
-
- 1. Pin the Widget
-
- Add the widget to your home screen once, then switch between client-side and server-side renders.
-
-
-
-
-
-
-
- 2. Choose the Render Path
-
- Both buttons target the same {WIDGET_ID} widget. Use them to compare how
- Material dynamic colors flow through the client and server pipelines.
-
-
-
-
-
-
-
-
- Preview
-
- This in-app preview mirrors the widget design. The home screen widget is the real test, but this makes it
- easier to see which render path you triggered last.
-
-
-
-
-
-
-
-
-
- Server Setup
- Run the example widget server before using the server render button:
-
- npm run widget:server --workspace voltra-example
-
-
- Android emulators use 10.0.2.2 in the widget config, so the built-in
- server-driven refresh hits your host machine automatically.
-
-
+
+
+ 1. Pin the Widget
+
+ Add the widget to your home screen once, then switch between client-side and server-side renders.
+
+
+
+
+
+
+
+ 2. Choose the Render Path
+
+ Both buttons target the same {WIDGET_ID} widget. Use them to compare how
+ Material dynamic colors flow through the client and server pipelines.
+
+
+
+
+
+
+
+
+ Preview
+
+ This in-app preview mirrors the widget design. The home screen widget is the real test, but this makes it
+ easier to see which render path you triggered last.
+
+
+
+
+
+
+
-
-
-
+
+ Android emulators use 10.0.2.2 in the widget config, so the built-in
+ server-driven refresh hits your host machine automatically.
+
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
buttonContainer: {
marginTop: 16,
},
diff --git a/example/screens/android/AndroidPreviewScreen.tsx b/example/screens/android/AndroidPreviewScreen.tsx
index 14c4660b..e28dcb04 100644
--- a/example/screens/android/AndroidPreviewScreen.tsx
+++ b/example/screens/android/AndroidPreviewScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { VoltraAndroid } from 'voltra/android'
-import { AndroidWidgetFamily, VoltraWidgetPreview } from 'voltra/android/client'
+import { VoltraAndroid } from '@use-voltra/android'
+import { AndroidWidgetFamily, VoltraWidgetPreview } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
const ANDROID_WIDGET_FAMILIES: { id: AndroidWidgetFamily; title: string; description: string }[] = [
{
@@ -102,94 +103,63 @@ export default function AndroidPreviewScreen() {
]
return (
-
-
- Widget Preview Testing
-
- Test how your Android widgets look at different sizes without leaving the app. These previews use the actual
- native Glance renderers for 100% accuracy.
-
-
- {/* Family Selection */}
-
- Widget Size: {ANDROID_WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.title}
- {ANDROID_WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.description}
-
-
- {ANDROID_WIDGET_FAMILIES.map((family) => (
-
-
-
-
- {/* Color Selection */}
-
- Customize Style
+
+
+ Widget Size: {ANDROID_WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.title}
+ {ANDROID_WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.description}
+
- {COLORS.map((color) => (
+ {ANDROID_WIDGET_FAMILIES.map((family) => (
-
+
+
- {/* Preview Area */}
-
- Live Preview
- This is rendered using a native Android View inside the app.
-
-
-
-
-
-
-
-
+
+ Customize Style
+
+ {COLORS.map((color) => (
+
+
- {/* Back Button */}
-
-
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 24,
- },
scrollArea: {
marginHorizontal: -4,
},
diff --git a/example/screens/android/AndroidScreen.tsx b/example/screens/android/AndroidScreen.tsx
index 1b126a67..9688392d 100644
--- a/example/screens/android/AndroidScreen.tsx
+++ b/example/screens/android/AndroidScreen.tsx
@@ -1,128 +1,16 @@
-import { useRouter } from 'expo-router'
-import React from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-
import { ActiveWidgetsAndroidCard } from '~/components/ActiveWidgetsAndroidCard'
-import { Button } from '~/components/Button'
-import { Card } from '~/components/Card'
-
-const ANDROID_SECTIONS = [
- {
- id: 'pin-widgets',
- title: 'Pin Widgets',
- description:
- 'Request to pin widgets directly from your app. Show the Android system pin widget dialog without leaving your app.',
- route: '/android-widgets/pin',
- },
- {
- id: 'image-preloading',
- title: 'Image Preloading',
- description:
- 'Test image preloading for Android Widgets. Download images to the app cache and verify they appear in your widgets.',
- route: '/android-widgets/image-preloading',
- },
- {
- id: 'image-fallback',
- title: 'Image Fallback',
- description:
- 'Test the new image fallback behavior using backgroundColor from styles. See how missing images render with different style properties.',
- route: '/android-widgets/image-fallback',
- },
- {
- id: 'preview-widgets',
- title: 'Widget Previews',
- description: 'Preview your Android widget layouts directly within the app using VoltraWidgetPreview.',
- route: '/android-widgets/preview',
- },
- {
- id: 'chart-widgets',
- title: 'Chart Widgets',
- description:
- 'Preview chart widgets rendered via Canvas bitmap. Supports bar, line, area, point, rule, and pie/donut charts.',
- route: '/android-widgets/charts',
- },
- {
- id: 'server-driven-widgets',
- title: 'Server-Driven Widgets',
- description:
- 'Serve dynamic widget content from a remote server using Voltra SSR. This example includes a sample widget server implementation.',
- route: '/android-widgets/server-driven',
- },
- {
- id: 'material-colors',
- title: 'Material Colors',
- description:
- 'Test one Android widget through both client-side and server-side rendering, using Material dynamic colors from the current device theme.',
- route: '/android-widgets/material-colors',
- },
- {
- id: 'custom-fonts',
- title: 'Custom Fonts',
- description:
- 'Render text with custom fonts in Android Glance widgets using bitmap rendering. Includes Pacifico (script) and Press Start 2P (pixel) demo.',
- route: '/android-widgets/custom-fonts',
- },
- {
- id: 'ongoing-notifications',
- title: 'Android Ongoing Notifications',
- description:
- 'Test semantic Android ongoing notifications with local start, update, upsert, stop, promotion capability checks, and real notification data payloads.',
- route: '/testing-grounds/android-ongoing-notification',
- },
- // Add more Android-specific sections here as they are implemented
-]
+import { ExampleSectionCards } from '~/components/ExampleSectionCards'
+import { ScreenLayout } from '~/components/ScreenLayout'
+import { ANDROID_WIDGET_SECTIONS } from '~/screens/android/tabs/sections'
export default function AndroidScreen() {
- const router = useRouter()
-
return (
-
-
- Voltra for Android
-
- Voltra for Android lets you build custom Android Widgets and ongoing notifications using React Native - no
- need to write Kotlin or XML anymore.
-
-
-
-
- {ANDROID_SECTIONS.map((section) => (
-
- {section.title}
- {section.description}
-
-
-
- ))}
-
-
+
+
+
+
)
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
- buttonContainer: {
- marginTop: 16,
- },
-})
diff --git a/example/screens/android/AndroidWidgetPinScreen.tsx b/example/screens/android/AndroidWidgetPinScreen.tsx
index b255c342..f2771085 100644
--- a/example/screens/android/AndroidWidgetPinScreen.tsx
+++ b/example/screens/android/AndroidWidgetPinScreen.tsx
@@ -1,9 +1,10 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, Platform, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'
-import { requestPinAndroidWidget } from 'voltra/android/client'
+import { Alert, Platform, StyleSheet, Text, TextInput, View } from 'react-native'
+import { requestPinAndroidWidget } from '@use-voltra/android-client'
import { Button } from '~/components/Button'
+import { ScreenLayout } from '~/components/ScreenLayout'
const AVAILABLE_WIDGETS = [
{
@@ -87,103 +88,83 @@ export default function AndroidWidgetPinScreen() {
}
return (
-
-
- Pin Widget to Home Screen
-
- Select a widget and pin it to your home screen. Optionally set preview dimensions to show how the widget will
- look.
-
-
-
- Widget
- {AVAILABLE_WIDGETS.map((widget) => (
-
-
- ))}
-
-
-
- Preview Dimensions (optional)
-
-
- Width (dp)
-
-
-
- Height (dp)
-
-
+
+
+ Widget
+ {AVAILABLE_WIDGETS.map((widget) => (
+
+
-
-
+ ))}
+
-
-
-
-
+ {isPinning && Requesting pin...}
+
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
section: {
marginBottom: 24,
},
@@ -193,17 +174,6 @@ const styles = StyleSheet.create({
color: '#FFFFFF',
marginBottom: 12,
},
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
widgetOption: {
marginTop: 12,
},
@@ -255,12 +225,6 @@ const styles = StyleSheet.create({
color: '#8232FF',
textAlign: 'center',
},
- noteText: {
- marginTop: 8,
- fontSize: 11,
- color: '#94A3B8',
- fontStyle: 'italic',
- },
footer: {
marginTop: 24,
alignItems: 'center',
diff --git a/example/screens/android/tabs/ActivityScreen.tsx b/example/screens/android/tabs/ActivityScreen.tsx
new file mode 100644
index 00000000..dae5f567
--- /dev/null
+++ b/example/screens/android/tabs/ActivityScreen.tsx
@@ -0,0 +1,5 @@
+import AndroidOngoingNotificationTestingScreen from '~/screens/testing-grounds/AndroidOngoingNotificationTestingScreen'
+
+export default function ActivityScreen() {
+ return
+}
diff --git a/example/screens/android/tabs/OthersScreen.tsx b/example/screens/android/tabs/OthersScreen.tsx
new file mode 100644
index 00000000..0ea1b832
--- /dev/null
+++ b/example/screens/android/tabs/OthersScreen.tsx
@@ -0,0 +1,14 @@
+import { ExampleSectionCards } from '~/components/ExampleSectionCards'
+import { ScreenLayout } from '~/components/ScreenLayout'
+import { ANDROID_OTHER_SECTIONS } from '~/screens/android/tabs/sections'
+
+export default function OthersScreen() {
+ return (
+
+
+
+ )
+}
diff --git a/example/screens/android/tabs/WidgetsScreen.tsx b/example/screens/android/tabs/WidgetsScreen.tsx
new file mode 100644
index 00000000..49babc10
--- /dev/null
+++ b/example/screens/android/tabs/WidgetsScreen.tsx
@@ -0,0 +1,5 @@
+import AndroidScreen from '~/screens/android/AndroidScreen'
+
+export default function WidgetsScreen() {
+ return
+}
diff --git a/example/screens/android/tabs/sections.ts b/example/screens/android/tabs/sections.ts
new file mode 100644
index 00000000..958276ac
--- /dev/null
+++ b/example/screens/android/tabs/sections.ts
@@ -0,0 +1,62 @@
+import type { ExampleSection } from '~/components/ExampleSectionCards'
+
+export const ANDROID_WIDGET_SECTIONS: ExampleSection[] = [
+ {
+ id: 'pin-widgets',
+ title: 'Pin Widgets',
+ description:
+ 'Request to pin widgets directly from your app. Show the Android system pin widget dialog without leaving your app.',
+ route: '/android-widgets/pin',
+ },
+ {
+ id: 'preview-widgets',
+ title: 'Widget Previews',
+ description: 'Preview your Android widget layouts directly within the app using VoltraWidgetPreview.',
+ route: '/android-widgets/preview',
+ },
+ {
+ id: 'chart-widgets',
+ title: 'Chart Widgets',
+ description:
+ 'Preview chart widgets rendered via Canvas bitmap. Supports bar, line, area, point, rule, and pie/donut charts.',
+ route: '/android-widgets/charts',
+ },
+ {
+ id: 'server-driven-widgets',
+ title: 'Server-Driven Widgets',
+ description:
+ 'Serve dynamic widget content from a remote server using Voltra SSR. This example includes a sample widget server implementation.',
+ route: '/android-widgets/server-driven',
+ },
+]
+
+export const ANDROID_OTHER_SECTIONS: ExampleSection[] = [
+ {
+ id: 'image-preloading',
+ title: 'Image Preloading',
+ description:
+ 'Test image preloading for Android widgets. Download images to the app cache and verify they appear in your widgets.',
+ route: '/android-widgets/image-preloading',
+ },
+ {
+ id: 'image-fallback',
+ title: 'Image Fallback',
+ description:
+ 'Test the image fallback behavior using backgroundColor from styles. See how missing images render with different style properties.',
+ route: '/android-widgets/image-fallback',
+ },
+ {
+ id: 'material-colors',
+ title: 'Material Colors',
+ description:
+ 'Test one Android widget through both client-side and server-side rendering, using Material dynamic colors from the current device theme.',
+ route: '/android-widgets/material-colors',
+ },
+ {
+ id: 'custom-fonts',
+ title: 'Custom Fonts',
+ description:
+ 'Render text with custom fonts in Android Glance widgets using bitmap rendering. Includes Pacifico and Press Start 2P demos.',
+ route: '/android-widgets/custom-fonts',
+ },
+]
diff --git a/example/screens/ios/tabs/ActivityScreen.tsx b/example/screens/ios/tabs/ActivityScreen.tsx
new file mode 100644
index 00000000..0980f065
--- /dev/null
+++ b/example/screens/ios/tabs/ActivityScreen.tsx
@@ -0,0 +1,5 @@
+import LiveActivitiesScreen from '~/screens/live-activities/LiveActivitiesScreen'
+
+export default function ActivityScreen() {
+ return
+}
diff --git a/example/screens/ios/tabs/OthersScreen.tsx b/example/screens/ios/tabs/OthersScreen.tsx
new file mode 100644
index 00000000..98091772
--- /dev/null
+++ b/example/screens/ios/tabs/OthersScreen.tsx
@@ -0,0 +1,14 @@
+import { ExampleSectionCards } from '~/components/ExampleSectionCards'
+import { ScreenLayout } from '~/components/ScreenLayout'
+import { IOS_OTHER_SECTIONS } from '~/screens/ios/tabs/sections'
+
+export default function OthersScreen() {
+ return (
+
+
+
+ )
+}
diff --git a/example/screens/ios/tabs/WidgetsScreen.tsx b/example/screens/ios/tabs/WidgetsScreen.tsx
new file mode 100644
index 00000000..e271f174
--- /dev/null
+++ b/example/screens/ios/tabs/WidgetsScreen.tsx
@@ -0,0 +1,16 @@
+import { ActiveWidgetsIOSCard } from '~/components/ActiveWidgetsIOSCard'
+import { ExampleSectionCards } from '~/components/ExampleSectionCards'
+import { ScreenLayout } from '~/components/ScreenLayout'
+import { IOS_WIDGET_SECTIONS } from '~/screens/ios/tabs/sections'
+
+export default function WidgetsScreen() {
+ return (
+
+
+
+
+ )
+}
diff --git a/example/screens/ios/tabs/sections.ts b/example/screens/ios/tabs/sections.ts
new file mode 100644
index 00000000..9b3b8d1d
--- /dev/null
+++ b/example/screens/ios/tabs/sections.ts
@@ -0,0 +1,104 @@
+import type { ExampleSection } from '~/components/ExampleSectionCards'
+
+export const IOS_WIDGET_SECTIONS: ExampleSection[] = [
+ {
+ id: 'weather',
+ title: 'Weather Widget',
+ description:
+ 'Test the weather widget with different conditions, gradients, and real-time updates. Change weather conditions and see the widget update instantly.',
+ route: '/testing-grounds/weather',
+ },
+ {
+ id: 'image-preloading',
+ title: 'Image Preloading',
+ description:
+ 'Test image preloading functionality for widgets. Download images to shared storage and verify they appear in widget renders.',
+ route: '/testing-grounds/image-preloading',
+ },
+ {
+ id: 'image-fallback',
+ title: 'Image Fallback',
+ description:
+ 'Explore the image fallback behavior using backgroundColor from styles. Test missing images with various styling approaches.',
+ route: '/testing-grounds/image-fallback',
+ },
+ {
+ id: 'widget-scheduling',
+ title: 'Widget Scheduling',
+ description:
+ 'Test widget timeline scheduling with multiple states. Configure timing for each state and watch widgets automatically transition between them.',
+ route: '/testing-grounds/widget-scheduling',
+ },
+ {
+ id: 'server-driven-widgets',
+ title: 'Server-Driven Widgets',
+ description:
+ 'Test server-driven widget updates. Widgets fetch fresh content from a remote server without the user opening the app. Manage auth credentials and trigger reloads.',
+ route: '/testing-grounds/server-driven-widgets',
+ },
+]
+
+export const IOS_OTHER_SECTIONS: ExampleSection[] = [
+ {
+ id: 'timer',
+ title: 'Timer',
+ description:
+ 'Test the VoltraTimer component with different styles (Timer/Relative), count directions, and templates. Verifies native Live Activity behavior.',
+ route: '/testing-grounds/timer',
+ },
+ {
+ id: 'styling',
+ title: 'Styling',
+ description:
+ 'Explore Voltra styling properties including padding, margins, colors, borders, shadows, and typography.',
+ route: '/testing-grounds/styling',
+ },
+ {
+ id: 'positioning',
+ title: 'Positioning',
+ description:
+ 'Learn about static, relative, and absolute positioning modes. See how left, top, and zIndex properties work with visual examples.',
+ route: '/testing-grounds/positioning',
+ },
+ {
+ id: 'progress',
+ title: 'Progress Indicators',
+ description:
+ 'Explore linear and circular progress indicators. Test determinate, indeterminate, and timer-based modes with custom labels and styling.',
+ route: '/testing-grounds/progress',
+ },
+ {
+ id: 'components',
+ title: 'Components',
+ description:
+ 'Explore all available Voltra components including Button, Text, VStack, HStack, ZStack, Image, and more.',
+ route: '/testing-grounds/components',
+ },
+ {
+ id: 'flex-playground',
+ title: 'Flex Layout Playground',
+ description:
+ 'Interactive playground for experimenting with flex layout properties. Test alignItems, justifyContent, flexDirection, spacing, and padding with live visual feedback.',
+ route: '/testing-grounds/flex-playground',
+ },
+ {
+ id: 'chart-playground',
+ title: 'Chart Playground',
+ description:
+ 'Explore all SwiftUI chart mark types: BarMark, LineMark, AreaMark, PointMark, RuleMark, and SectorMark. Randomize data to see animated transitions.',
+ route: '/testing-grounds/chart-playground',
+ },
+ {
+ id: 'gradient-playground',
+ title: 'Gradient Playground',
+ description:
+ 'Test CSS gradient strings as backgroundColor. Experiment with linear, radial, and conic gradients, direction and angle controls, color presets, stop positions, and borderRadius clipping.',
+ route: '/testing-grounds/gradient-playground',
+ },
+ {
+ id: 'channel-updates',
+ title: 'Channel-Based Updates',
+ description: 'Start a minimal Live Activity bound to a broadcast channel ID and test server-driven updates.',
+ route: '/testing-grounds/channel-updates',
+ },
+]
diff --git a/example/screens/live-activities/BasicAndroidOngoingNotification.tsx b/example/screens/live-activities/BasicAndroidOngoingNotification.tsx
index 5796eba9..8ff57637 100644
--- a/example/screens/live-activities/BasicAndroidOngoingNotification.tsx
+++ b/example/screens/live-activities/BasicAndroidOngoingNotification.tsx
@@ -1,6 +1,6 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
-import { AndroidOngoingNotification } from 'voltra/android'
-import { useAndroidOngoingNotification } from 'voltra/android/client'
+import { AndroidOngoingNotification } from '@use-voltra/android'
+import { useAndroidOngoingNotification } from '@use-voltra/android-client'
import { LiveActivityExampleComponent } from './types'
diff --git a/example/screens/live-activities/BasicLiveActivity.tsx b/example/screens/live-activities/BasicLiveActivity.tsx
index 7629b54f..bce8dd8c 100644
--- a/example/screens/live-activities/BasicLiveActivity.tsx
+++ b/example/screens/live-activities/BasicLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import { BasicLiveActivityUI } from '../../components/live-activities/BasicLiveActivityUI'
import { LiveActivityExampleComponent } from './types'
diff --git a/example/screens/live-activities/CompassLiveActivity.tsx b/example/screens/live-activities/CompassLiveActivity.tsx
index eaa308a3..49c62d08 100644
--- a/example/screens/live-activities/CompassLiveActivity.tsx
+++ b/example/screens/live-activities/CompassLiveActivity.tsx
@@ -1,7 +1,7 @@
import { Magnetometer } from 'expo-sensors'
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { Alert } from 'react-native'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import {
CompassLiveActivityIslandCompactLeading,
diff --git a/example/screens/live-activities/DeepLinksLiveActivity.tsx b/example/screens/live-activities/DeepLinksLiveActivity.tsx
index f76fb571..3a552715 100644
--- a/example/screens/live-activities/DeepLinksLiveActivity.tsx
+++ b/example/screens/live-activities/DeepLinksLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import { DeepLinksLiveActivityUI } from '../../components/live-activities/DeepLinksLiveActivityUI'
import { LiveActivityExampleComponent } from './types'
diff --git a/example/screens/live-activities/FlightLiveActivity.tsx b/example/screens/live-activities/FlightLiveActivity.tsx
index 1d505434..763265ad 100644
--- a/example/screens/live-activities/FlightLiveActivity.tsx
+++ b/example/screens/live-activities/FlightLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import {
FlightLiveActivityIslandCompactLeading,
diff --git a/example/screens/live-activities/LiquidGlassLiveActivity.tsx b/example/screens/live-activities/LiquidGlassLiveActivity.tsx
index b05eb03e..8420080d 100644
--- a/example/screens/live-activities/LiquidGlassLiveActivity.tsx
+++ b/example/screens/live-activities/LiquidGlassLiveActivity.tsx
@@ -1,6 +1,6 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
-import { Voltra } from 'voltra'
-import { useLiveActivity } from 'voltra/client'
+import { Voltra } from '@use-voltra/ios'
+import { useLiveActivity } from '@use-voltra/ios-client'
import {
LiquidGlassLiveActivityUI,
diff --git a/example/screens/live-activities/LiveActivitiesScreen.tsx b/example/screens/live-activities/LiveActivitiesScreen.tsx
index b32fb2c5..62444d4a 100644
--- a/example/screens/live-activities/LiveActivitiesScreen.tsx
+++ b/example/screens/live-activities/LiveActivitiesScreen.tsx
@@ -1,12 +1,11 @@
-import { Link } from 'expo-router'
import React, { useCallback, useMemo, useRef, useState } from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { endAllLiveActivities } from 'voltra/client'
+import { StyleSheet, Text, View } from 'react-native'
+import { endAllLiveActivities } from '@use-voltra/ios-client'
-import { ActiveWidgetsIOSCard } from '~/components/ActiveWidgetsIOSCard'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
import { NotificationsCard } from '~/components/NotificationsCard'
+import { ScreenLayout } from '~/components/ScreenLayout'
import BasicLiveActivity from '~/screens/live-activities/BasicLiveActivity'
import CompassLiveActivity from '~/screens/live-activities/CompassLiveActivity'
import DeepLinksLiveActivity from '~/screens/live-activities/DeepLinksLiveActivity'
@@ -195,76 +194,32 @@ export default function LiveActivitiesScreen() {
}
return (
-
-
- Voltra
-
- Voltra is a library that lets you build custom Live Activities and Dynamic Island layouts using React Native -
- no need to open Xcode anymore.
-
+
+
-
-
-
-
-
-
-
-
-
-
- {CARD_ORDER.map(renderCard)}
+ {CARD_ORDER.map(renderCard)}
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
- navigationButtons: {
- marginTop: 16,
- flexDirection: 'row',
- gap: 12,
- },
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/example/screens/live-activities/MusicPlayerLiveActivity.tsx b/example/screens/live-activities/MusicPlayerLiveActivity.tsx
index 94ae2414..8b9a9590 100644
--- a/example/screens/live-activities/MusicPlayerLiveActivity.tsx
+++ b/example/screens/live-activities/MusicPlayerLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
-import { addVoltraListener, useLiveActivity } from 'voltra/client'
+import { addVoltraListener, useLiveActivity } from '@use-voltra/ios-client'
import { MusicPlayerLiveActivityUI, SONGS } from '../../components/live-activities/MusicPlayerLiveActivityUI'
import { LiveActivityExampleComponent } from './types'
diff --git a/example/screens/live-activities/SupplementalFamiliesLiveActivity.tsx b/example/screens/live-activities/SupplementalFamiliesLiveActivity.tsx
index 1237bdbc..027119a6 100644
--- a/example/screens/live-activities/SupplementalFamiliesLiveActivity.tsx
+++ b/example/screens/live-activities/SupplementalFamiliesLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import {
SupplementalFamiliesCompactLeading,
diff --git a/example/screens/live-activities/WorkoutLiveActivity.tsx b/example/screens/live-activities/WorkoutLiveActivity.tsx
index ce790283..35cb61af 100644
--- a/example/screens/live-activities/WorkoutLiveActivity.tsx
+++ b/example/screens/live-activities/WorkoutLiveActivity.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
-import { useLiveActivity } from 'voltra/client'
+import { useLiveActivity } from '@use-voltra/ios-client'
import { WorkoutLiveActivityUI } from '../../components/live-activities/WorkoutLiveActivityUI'
import { LiveActivityExampleComponent } from './types'
diff --git a/example/screens/shared/ServerDrivenWidgetsScreen.tsx b/example/screens/shared/ServerDrivenWidgetsScreen.tsx
index bd1b3c66..4797472f 100644
--- a/example/screens/shared/ServerDrivenWidgetsScreen.tsx
+++ b/example/screens/shared/ServerDrivenWidgetsScreen.tsx
@@ -1,15 +1,21 @@
import React, { useState } from 'react'
-import { Alert, Platform, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'
-import { reloadAndroidWidgets, VoltraWidgetPreview as AndroidVoltraWidgetPreview } from 'voltra/android/client'
+import { Alert, Platform, StyleSheet, Text, TextInput, View } from 'react-native'
+import {
+ clearWidgetServerCredentials as clearWidgetServerCredentialsAndroid,
+ reloadWidgets as reloadWidgetsAndroid,
+ setWidgetServerCredentials as setWidgetServerCredentialsAndroid,
+ VoltraWidgetPreview as VoltraWidgetPreviewAndroid,
+} from '@use-voltra/android-client'
import {
clearWidgetServerCredentials,
reloadWidgets,
setWidgetServerCredentials,
VoltraWidgetPreview,
-} from 'voltra/client'
+} from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { AndroidPortfolioWidget } from '~/widgets/android/AndroidPortfolioWidget'
import { IosPortfolioWidget } from '~/widgets/ios/IosPortfolioWidget'
@@ -22,12 +28,21 @@ export default function ServerDrivenWidgetsScreen() {
const handleSetCredentials = async () => {
setIsLoading(true)
try {
- await setWidgetServerCredentials({
- token,
- headers: {
- 'X-Widget-Source': 'voltra-example',
- },
- })
+ if (Platform.OS === 'android') {
+ await setWidgetServerCredentialsAndroid({
+ token,
+ headers: {
+ 'X-Widget-Source': 'voltra-example',
+ },
+ })
+ } else {
+ await setWidgetServerCredentials({
+ token,
+ headers: {
+ 'X-Widget-Source': 'voltra-example',
+ },
+ })
+ }
setCredentialsSet(true)
Alert.alert(
'Success',
@@ -43,7 +58,11 @@ export default function ServerDrivenWidgetsScreen() {
const handleClearCredentials = async () => {
setIsLoading(true)
try {
- await clearWidgetServerCredentials()
+ if (Platform.OS === 'android') {
+ await clearWidgetServerCredentialsAndroid()
+ } else {
+ await clearWidgetServerCredentials()
+ }
setCredentialsSet(false)
Alert.alert('Success', 'Widget server credentials cleared.')
} catch (error) {
@@ -56,7 +75,7 @@ export default function ServerDrivenWidgetsScreen() {
const handleReloadWidgets = async () => {
try {
if (Platform.OS === 'android') {
- await reloadAndroidWidgets(['portfolio'])
+ await reloadWidgetsAndroid(['portfolio'])
Alert.alert('Success', 'Android widgets reloaded. WorkManager will fetch fresh content from the server.')
} else {
await reloadWidgets(['portfolio'])
@@ -68,46 +87,42 @@ export default function ServerDrivenWidgetsScreen() {
}
return (
-
-
- Server-Driven Widgets
-
- Widgets can fetch content from a remote server without the user opening the app. This is configured via the{' '}
- serverUpdate option in the plugin config.
-
+
+
+ Widgets can fetch content from a remote server without the user opening the app. This is configured via the{' '}
+ serverUpdate option in the plugin config.
+
- {/* How it works */}
-
- How it works
-
- 1. Configure serverUpdate.url in your widget config{'\n\n'}
- 2. Call setWidgetServerCredentials() after user login{'\n\n'}
- 3. iOS WidgetKit / Android WorkManager periodically fetches from your server URL{'\n\n'}
- 4. Your server renders JSX → JSON using createWidgetUpdateHandler()
- {'\n\n'}
- 5. The widget updates automatically — no app launch needed!
-
- {Platform.OS === 'android' ? (
-
-
- ⚠️ Android emulator: use 10.0.2.2 instead of localhost to reach the host machine. Real devices need the
- host`s LAN IP.
-
-
- ) : null}
-
+
+ How it works
+
+ 1. Configure serverUpdate.url in your widget config{`\n\n`}
+ 2. Call setWidgetServerCredentials() after user login{`\n\n`}
+ 3. iOS WidgetKit / Android WorkManager periodically fetches from your server URL{`\n\n`}
+ 4. Your server renders JSX → JSON using createWidgetUpdateHandler()
+ {`\n\n`}
+ 5. The widget updates automatically — no app launch needed!
+
+ {Platform.OS === 'android' ? (
+
+
+ ⚠️ Android emulator: use 10.0.2.2 instead of localhost to reach the host machine. Real devices need the
+ host`s LAN IP.
+
+
+ ) : null}
+
- {/* Plugin config */}
-
- Plugin Configuration
-
- In app.json, add serverUpdate to your
- widget:
-
-
-
- {Platform.OS === 'android'
- ? `// android.widgets in app.json
+
+ Plugin Configuration
+
+ In app.json, add serverUpdate to your
+ widget:
+
+
+
+ {Platform.OS === 'android'
+ ? `// android.widgets in app.json
{
"android": {
"widgets": [{
@@ -119,7 +134,7 @@ export default function ServerDrivenWidgetsScreen() {
}]
}
}`
- : `// widgets in app.json (iOS)
+ : `// widgets in app.json (iOS)
{
"widgets": [{
"id": "portfolio",
@@ -129,131 +144,113 @@ export default function ServerDrivenWidgetsScreen() {
}
}]
}`}
-
-
-
+
+
+
- {/* Credentials */}
-
- Server Credentials
-
- Store auth tokens securely so the widget extension can authenticate with your server in the background.
-
+
+ Server Credentials
+
+ Store auth tokens securely so the widget extension can authenticate with your server in the background.
+
- Server URL
-
+ Server URL
+
- Auth Token
-
+ Auth Token
+
-
-
-
-
-
+
+
+
+
+
- {/* Actions */}
-
- Widget Actions
-
- Reload widget timelines to trigger an immediate server fetch. Normally the widget fetches at the configured
- interval.
-
-
-
-
-
+
+ Widget Actions
+
+ Reload widget timelines to trigger an immediate server fetch. Normally the widget fetches at the configured
+ interval.
+
+
+
+
+
- {/* Server setup */}
-
- Running the Server
- Start the example widget server in a terminal:
-
- npx tsx example/server/widget-server.tsx
-
-
- {'\n'}The server renders portfolio widgets with randomized chart data. Each request returns different
- portfolio performance data so you can see the widget update.{'\n\n'}
- iOS uses Voltra.* components while Android uses{' '}
- VoltraAndroid.* components. The server handles both via separate{' '}
- render and renderAndroid callbacks.
-
-
+
+ Running the Server
+ Start the example widget server in a terminal:
+
+ npx tsx example/server/widget-server.tsx
+
+
+ {`\n`}The server renders portfolio widgets with randomized chart data. Each request returns different
+ portfolio performance data so you can see the widget update.{`\n\n`}
+ iOS uses Voltra.* components while Android uses{' '}
+ VoltraAndroid.* components. The server handles both via separate{' '}
+ render and renderAndroid callbacks.
+
+
- {/* Widget preview */}
-
- Widget Preview
-
- This is the portfolio widget`s initial state. When serverUpdate is
- configured, the widget extension will periodically replace this with fresh server-rendered content.
-
-
- {Platform.OS === 'android' ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
+
+ Widget Preview
+
+ This is the portfolio widget`s initial state. When serverUpdate is
+ configured, the widget extension will periodically replace this with fresh server-rendered content.
+
+
+ {Platform.OS === 'android' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
content: {
- padding: 16,
- paddingBottom: 40,
+ paddingHorizontal: 16,
+ paddingTop: 16,
},
- heading: {
- fontSize: 28,
- fontWeight: '700',
- color: '#E2E8F0',
- marginBottom: 4,
- },
- subheading: {
+ description: {
fontSize: 14,
color: '#94A3B8',
lineHeight: 20,
+ marginBottom: 16,
},
code: {
fontFamily: 'Courier',
@@ -310,7 +307,4 @@ const styles = StyleSheet.create({
borderRadius: 16,
overflow: 'hidden',
},
- bottomSpacer: {
- height: 40,
- },
})
diff --git a/example/screens/testing-grounds/AndroidOngoingNotificationTestingScreen.tsx b/example/screens/testing-grounds/AndroidOngoingNotificationTestingScreen.tsx
index 48f8ddb1..81309ddc 100644
--- a/example/screens/testing-grounds/AndroidOngoingNotificationTestingScreen.tsx
+++ b/example/screens/testing-grounds/AndroidOngoingNotificationTestingScreen.tsx
@@ -1,11 +1,11 @@
import { useFocusEffect } from 'expo-router'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
-import { AppState, PermissionsAndroid, Platform, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'
+import { AppState, PermissionsAndroid, Platform, StyleSheet, Text, TextInput, View } from 'react-native'
import {
AndroidOngoingNotification,
type AndroidOngoingNotificationPayload,
type StartAndroidOngoingNotificationOptions,
-} from 'voltra/android'
+} from '@use-voltra/android'
import {
getAndroidOngoingNotificationCapabilities,
isAndroidOngoingNotificationActive,
@@ -14,10 +14,11 @@ import {
stopAndroidOngoingNotification,
upsertAndroidOngoingNotification,
updateAndroidOngoingNotification,
-} from 'voltra/android/client'
+} from '@use-voltra/android-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
type OngoingNotificationStyle = 'progress' | 'bigText'
@@ -345,371 +346,354 @@ export default function AndroidOngoingNotificationTestingScreen() {
if (Platform.OS !== 'android') {
return (
-
-
- Android Ongoing Notifications
- This testing ground is only available on Android.
-
-
+
)
}
return (
-
-
- Android Ongoing Notifications
-
- Test semantic Android ongoing notification payloads, capability checks, rich progress fields, and action
- buttons. For remote testing, send a real notification whose `data` contains the payload shown below.
-
-
-
- Rich Progress Fallback
-
- Segments, points, and tracker/start/end icons are applied on API 36+ only and ignored silently on older
- Android versions.
-
- Large icon and subtext remain available outside that richer path.
-
-
-
- Action Buttons
- Action children launch their own deep links.
-
- Action icons are still wired into the payload, but standard Android notification UI usually does not render
- them.
-
- Use the icon fields here to verify payload support, not visible action-button artwork.
- Unknown children are ignored, so this screen only renders explicit action entries.
-
-
-
- Runtime Capabilities
- {`API ${capabilities.apiLevel} • notifications ${
- capabilities.notificationsEnabled ? 'enabled' : 'disabled'
- }`}
- {`Promoted support: ${capabilities.supportsPromotedNotifications ? 'yes' : 'no'}`}
- {`Can post promoted notifications: ${
- capabilities.canPostPromotedNotifications ? 'yes' : 'no'
- }`}
- {`Can request promoted ongoing: ${
- capabilities.canRequestPromotedOngoing ? 'yes' : 'no'
- }`}
-
+
+
+ Rich Progress Fallback
+
+ Segments, points, and tracker/start/end icons are applied on API 36+ only and ignored silently on older
+ Android versions.
+
+ Large icon and subtext remain available outside that richer path.
+
+
+
+ Action Buttons
+ Action children launch their own deep links.
+
+ Action icons are still wired into the payload, but standard Android notification UI usually does not render
+ them.
+
+ Use the icon fields here to verify payload support, not visible action-button artwork.
+ Unknown children are ignored, so this screen only renders explicit action entries.
+
+
+
+ Runtime Capabilities
+ {`API ${capabilities.apiLevel} • notifications ${
+ capabilities.notificationsEnabled ? 'enabled' : 'disabled'
+ }`}
+ {`Promoted support: ${capabilities.supportsPromotedNotifications ? 'yes' : 'no'}`}
+ {`Can post promoted notifications: ${
+ capabilities.canPostPromotedNotifications ? 'yes' : 'no'
+ }`}
+ {`Can request promoted ongoing: ${
+ capabilities.canRequestPromotedOngoing ? 'yes' : 'no'
+ }`}
+
+
+
+ {permissionStatus ? {permissionStatus} : null}
+
+
+
+ Ongoing Notification Target
+
+ Notification ID
+
+
+
+ Channel ID
+
+
+
+ Small Icon
+
+
+
+ Request Promoted Ongoing
+ setRequestPromotedOngoing((current) => !current)}
+ style={styles.smButton}
+ />
+
+
+ Active State
+
+ {activeState ? 'Active' : 'Idle'}
+
+
+
+
+
+
+ Semantic Content
+
+ Style
+
-
- {permissionStatus ? {permissionStatus} : null}
-
-
-
- Ongoing Notification Target
-
- Notification ID
- setStyle('progress')}
+ style={styles.smButton}
/>
-
-
- Channel ID
-
-
-
- Small Icon
-
-
-
- Request Promoted Ongoing
setRequestPromotedOngoing((current) => !current)}
+ title="Big Text"
+ variant={style === 'bigText' ? 'primary' : 'secondary'}
+ onPress={() => setStyle('bigText')}
style={styles.smButton}
/>
-
- Active State
-
- {activeState ? 'Active' : 'Idle'}
-
-
-
-
-
-
- Semantic Content
-
- Style
-
- setStyle('progress')}
- style={styles.smButton}
+
+
+
+ Title
+
+
+
+ Text
+
+
+
+ Sub Text
+
+
+
+ Short Critical
+
+
+
+ Large Icon
+
+
+
+ Chronometer
+ setChronometer((current) => !current)}
+ style={styles.smButton}
+ />
+
+
+ {style === 'progress' ? (
+ <>
+
+ Value
+
+
+
+ Max
+
+
+
+ Indeterminate
setStyle('bigText')}
+ title={indeterminate ? 'ON' : 'OFF'}
+ variant={indeterminate ? 'primary' : 'secondary'}
+ onPress={() => setIndeterminate((current) => !current)}
style={styles.smButton}
/>
-
-
-
- Title
-
-
-
- Text
-
-
-
- Sub Text
-
-
-
- Short Critical
-
-
-
- Large Icon
-
-
-
- Chronometer
- setChronometer((current) => !current)}
- style={styles.smButton}
- />
-
-
- {style === 'progress' ? (
- <>
+
+ Tracker Icon
+
+
+
+ Start Icon
+
+
+
+ End Icon
+
+
+
+ Segments JSON
+
+
+
+ Points JSON
+
+
+
+ Action Buttons
+
+ Primary Title
+
+
- Value
+ Primary Deep Link
- Max
+ Primary Icon
- Indeterminate
- setIndeterminate((current) => !current)}
- style={styles.smButton}
- />
+ Secondary Title
+
- Tracker Icon
+ Secondary Deep Link
+
+ >
+ ) : (
+ <>
+
+ Big Text
+
+
+
+ Action Buttons
+
+ Primary Title
+
+
- Start Icon
+ Primary Deep Link
- End Icon
+ Primary Icon
-
- Segments JSON
-
-
-
- Points JSON
-
-
-
- Action Buttons
-
- Primary Title
-
-
-
- Primary Deep Link
-
-
-
- Primary Icon
-
-
-
- Secondary Title
-
-
-
- Secondary Deep Link
-
-
+
+ Secondary Title
+
- >
- ) : (
- <>
-
- Big Text
+
+ Secondary Deep Link
-
- Action Buttons
-
- Primary Title
-
-
-
- Primary Deep Link
-
-
-
- Primary Icon
-
-
-
- Secondary Title
-
-
-
- Secondary Deep Link
-
-
-
- >
- )}
-
-
-
- Actions
-
-
-
-
-
-
-
- {statusMessage ? {statusMessage} : null}
-
-
-
- Rendered Payload
-
- {renderedPayload || 'Render a payload to inspect the semantic snapshot.'}
-
-
-
-
- Real Notification Test
-
- Send a real high-priority notification whose `data.voltraOngoingNotification` contains this envelope. The
- example's registered background task will parse it and upsert the ongoing notification.
-
- Use `operation: "stop"` with the same `notificationId` to stop it remotely.
- {sampleExpoPushRequest}
-
-
-
+
+ >
+ )}
+
+
+
+ Actions
+
+
+
+
+
+
+
+ {statusMessage ? {statusMessage} : null}
+
+
+
+ Rendered Payload
+ {renderedPayload || 'Render a payload to inspect the semantic snapshot.'}
+
+
+
+ Real Notification Test
+
+ Send a real high-priority notification whose `data.voltraOngoingNotification` contains this envelope. The
+ example's registered background task will parse it and upsert the ongoing notification.
+
+ Use `operation: "stop"` with the same `notificationId` to stop it remotely.
+ {sampleExpoPushRequest}
+
+
)
}
const styles = StyleSheet.create({
- container: { flex: 1 },
- scrollView: { flex: 1 },
- content: { padding: 20 },
- heading: { fontSize: 24, fontWeight: '700', color: '#FFFFFF', marginBottom: 8 },
- subheading: { fontSize: 14, color: '#CBD5F5', marginBottom: 24, lineHeight: 20 },
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, gap: 12 },
column: { marginBottom: 16, gap: 10 },
label: { color: '#FFFFFF', fontSize: 16, flexShrink: 0 },
diff --git a/example/screens/testing-grounds/ChannelUpdatesTestingScreen.tsx b/example/screens/testing-grounds/ChannelUpdatesTestingScreen.tsx
index 1588775c..8e9df1c1 100644
--- a/example/screens/testing-grounds/ChannelUpdatesTestingScreen.tsx
+++ b/example/screens/testing-grounds/ChannelUpdatesTestingScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useMemo, useState } from 'react'
-import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { useLiveActivity } from 'voltra/client'
+import { Alert, Platform, StyleSheet, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { useLiveActivity } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { TextInput } from '~/components/TextInput'
export default function ChannelUpdatesTestingScreen() {
@@ -55,63 +56,40 @@ export default function ChannelUpdatesTestingScreen() {
}
return (
-
-
- Channel-Based Updates
- Start a minimal Live Activity subscribed to a specific broadcast channel.
-
-
- Live Activity Channel
- Use an APNs broadcast channel ID for this activity.
-
-
-
-
-
-
-
-
+
+
+ Live Activity Channel
+ Use an APNs broadcast channel ID for this activity.
+
+
+
-
- router.push('/testing-grounds')} />
+
+
-
-
+
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
inputContainer: {
marginTop: 16,
},
diff --git a/example/screens/testing-grounds/ImageFallbackScreen.tsx b/example/screens/testing-grounds/ImageFallbackScreen.tsx
index 48c9b34b..b8601f8a 100644
--- a/example/screens/testing-grounds/ImageFallbackScreen.tsx
+++ b/example/screens/testing-grounds/ImageFallbackScreen.tsx
@@ -1,13 +1,16 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React from 'react'
-import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { startLiveActivity } from 'voltra/client'
+import { Alert, StyleSheet, Text, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { startLiveActivity } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
export default function ImageFallbackScreen() {
+ const router = useRouter()
+
const handleShowExample = async (exampleName: string, content: React.ReactElement) => {
try {
await startLiveActivity(
@@ -25,330 +28,302 @@ export default function ImageFallbackScreen() {
}
return (
-
-
- Image Fallback with Styles
-
- Test the new image fallback behavior using backgroundColor from styles instead of the deprecated fallbackColor
- prop.
-
-
-
- 1. Missing Image with Background Color
-
- When an image is missing and no fallback component is provided, the backgroundColor from styles is used.
-
-
-
- handleShowExample(
- 'Background Color',
-
-
- Missing Image Test
-
-
-
-
-
- Red Background
-
- backgroundColor: '#EF4444'
-
-
-
- )
- }
- />
-
-
-
-
- 2. Multiple Fallback Colors
-
- Display multiple missing images with different background colors to demonstrate the style-based approach.
-
-
-
- handleShowExample(
- 'Multiple Colors',
-
-
- Color Palette
-
-
-
-
-
-
-
-
- )
- }
- />
-
-
-
-
- 3. Transparent Fallback (No Background)
-
- When no backgroundColor is specified, the fallback is transparent, allowing the parent background to show
- through.
-
-
-
- handleShowExample(
- 'Transparent',
-
-
- Transparent Fallback
-
+
+
+ 1. Missing Image with Background Color
+
+ When an image is missing and no fallback component is provided, the backgroundColor from styles is used.
+
+
+
+ handleShowExample(
+ 'Background Color',
+
+
+ Missing Image Test
+
+
-
- No backgroundColor - parent color shows through
-
-
- )
- }
- />
-
-
+
+
+ Red Background
+
+ backgroundColor: '#EF4444'
+
+
+
+ )
+ }
+ />
+
+
-
- 4. Combined Style Properties
-
- Apply multiple style properties to the fallback including backgroundColor, borderRadius, borders, and
- shadows.
-
-
-
- handleShowExample(
- 'Combined Styles',
-
-
- Styled Fallback
-
+
+ 2. Multiple Fallback Colors
+
+ Display multiple missing images with different background colors to demonstrate the style-based approach.
+
+
+
+ handleShowExample(
+ 'Multiple Colors',
+
+
+ Color Palette
+
+
-
- backgroundColor + borderRadius + borders
-
-
- )
- }
- />
-
-
-
-
- 5. Custom Fallback Component
-
- Use a custom fallback component with icons or text. The styles apply to the container, not the fallback
- content.
-
-
-
- handleShowExample(
- 'Custom Fallback',
-
-
- Custom Fallback
-
-
- No Image
-
- }
+ source={{ assetName: 'missing-2' }}
style={{
- width: 100,
- height: 100,
- backgroundColor: '#1E293B',
- borderRadius: 12,
- borderWidth: 2,
- borderColor: '#334155',
+ width: 50,
+ height: 50,
+ backgroundColor: '#F59E0B',
+ borderRadius: 8,
+ }}
+ />
+
+
-
- )
- }
- />
-
-
+
+
+ )
+ }
+ />
+
+
+
+
+ 3. Transparent Fallback (No Background)
+
+ When no backgroundColor is specified, the fallback is transparent, allowing the parent background to show
+ through.
+
+
+
+ handleShowExample(
+ 'Transparent',
+
+
+ Transparent Fallback
+
+
+
+ No backgroundColor - parent color shows through
+
+
+ )
+ }
+ />
+
+
-
- 6. Mixed: Valid and Missing Images
-
- Display a mix of valid and missing images to show consistent styling behavior. Valid images display normally
- while missing ones show the styled fallback.
-
-
-
- handleShowExample(
- 'Mixed Images',
-
- Image Grid
-
-
-
-
-
-
- All missing - styled with different colors
-
-
- )
- }
- />
-
-
+
+ 4. Combined Style Properties
+
+ Apply multiple style properties to the fallback including backgroundColor, borderRadius, borders, and shadows.
+
+
+
+ handleShowExample(
+ 'Combined Styles',
+
+
+ Styled Fallback
+
+
+
+ backgroundColor + borderRadius + borders
+
+
+ )
+ }
+ />
+
+
-
- Migration Note
-
- The fallbackColor prop has been removed. Use{' '}
- backgroundColor in the style prop instead.
-
-
- Before: fallbackColor="#E0E0E0"
-
-
- After: style={'{{ backgroundColor: "#E0E0E0" }}'}
-
+
+ 5. Custom Fallback Component
+
+ Use a custom fallback component with icons or text. The styles apply to the container, not the fallback
+ content.
+
+
+
+ handleShowExample(
+ 'Custom Fallback',
+
+
+ Custom Fallback
+
+
+
+ No Image
+
+ }
+ style={{
+ width: 100,
+ height: 100,
+ backgroundColor: '#1E293B',
+ borderRadius: 12,
+ borderWidth: 2,
+ borderColor: '#334155',
+ }}
+ />
+
+ )
+ }
+ />
+
-
-
-
-
+
+ 6. Mixed: Valid and Missing Images
+
+ Display a mix of valid and missing images to show consistent styling behavior. Valid images display normally
+ while missing ones show the styled fallback.
+
+
+
+ handleShowExample(
+ 'Mixed Images',
+
+ Image Grid
+
+
+
+
+
+
+ All missing - styled with different colors
+
+
+ )
+ }
+ />
+
+
+
+ Migration Note
+
+ The fallbackColor prop has been removed. Use{' '}
+ backgroundColor in the style prop instead.
+
+
+ Before: fallbackColor="#E0E0E0"
+
+
+ After: style={'{{ backgroundColor: "#E0E0E0" }}'}
+
-
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#0B0F1A',
- },
- scrollView: {
- flex: 1,
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
buttonRow: {
marginTop: 16,
flexDirection: 'column',
diff --git a/example/screens/testing-grounds/ImagePreloadingScreen.tsx b/example/screens/testing-grounds/ImagePreloadingScreen.tsx
index bef9e9ec..08d1a026 100644
--- a/example/screens/testing-grounds/ImagePreloadingScreen.tsx
+++ b/example/screens/testing-grounds/ImagePreloadingScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { clearPreloadedImages, preloadImages, reloadLiveActivities, startLiveActivity } from 'voltra/client'
+import { Alert, StyleSheet, Text, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { clearPreloadedImages, preloadImages, reloadLiveActivities, startLiveActivity } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { TextInput } from '~/components/TextInput'
function generateRandomKey(): string {
@@ -13,6 +14,7 @@ function generateRandomKey(): string {
}
export default function ImagePreloadingScreen() {
+ const router = useRouter()
const [url, setUrl] = useState(`https://picsum.photos/id/${Math.floor(Math.random() * 120)}/100/100`)
const [isProcessing, setIsProcessing] = useState(false)
const [currentAssetKey, setCurrentAssetKey] = useState(null)
@@ -92,72 +94,45 @@ export default function ImagePreloadingScreen() {
}
return (
-
-
- Image Preloading
-
- Test image preloading functionality for Live Activities. Download images to App Group storage and verify they
- appear in Live Activities.
-
-
-
- Show and Download
- Enter a URL to start a Live Activity and preload the image automatically.
-
-
- Image URL
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Show and Download
+ Enter a URL to start a Live Activity and preload the image automatically.
+
+
+ Image URL
+
+
+
+
+
+
+
+
+
+ router.back()} />
-
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#0B0F1A',
- },
- scrollView: {
- flex: 1,
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
inputContainer: {
marginTop: 16,
},
diff --git a/example/screens/testing-grounds/TestingGroundsScreen.tsx b/example/screens/testing-grounds/TestingGroundsScreen.tsx
index 6d427971..0a7500a3 100644
--- a/example/screens/testing-grounds/TestingGroundsScreen.tsx
+++ b/example/screens/testing-grounds/TestingGroundsScreen.tsx
@@ -1,172 +1,8 @@
-import { useRouter } from 'expo-router'
-import React from 'react'
-import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
+import { Platform } from 'react-native'
-import { Button } from '~/components/Button'
-import { Card } from '~/components/Card'
-
-const TESTING_GROUNDS_SECTIONS = [
- {
- id: 'weather',
- title: 'Weather Widget',
- description:
- 'Test the weather widget with different conditions, gradients, and real-time updates. Change weather conditions and see the widget update instantly.',
- route: '/testing-grounds/weather',
- },
- {
- id: 'timer',
- title: 'Timer',
- description:
- 'Test the VoltraTimer component with different styles (Timer/Relative), count directions, and templates. Verifies native Live Activity behavior.',
- route: '/testing-grounds/timer',
- },
- {
- id: 'styling',
- title: 'Styling',
- description:
- 'Explore Voltra styling properties including padding, margins, colors, borders, shadows, and typography.',
- route: '/testing-grounds/styling',
- },
- {
- id: 'positioning',
- title: 'Positioning',
- description:
- 'Learn about static, relative, and absolute positioning modes. See how left, top, and zIndex properties work with visual examples.',
- route: '/testing-grounds/positioning',
- },
- {
- id: 'progress',
- title: 'Progress Indicators',
- description:
- 'Explore linear and circular progress indicators. Test determinate, indeterminate, and timer-based modes with custom labels and styling.',
- route: '/testing-grounds/progress',
- },
- {
- id: 'components',
- title: 'Components',
- description:
- 'Explore all available Voltra components including Button, Text, VStack, HStack, ZStack, Image, and more.',
- route: '/testing-grounds/components',
- },
- {
- id: 'flex-playground',
- title: 'Flex Layout Playground',
- description:
- 'Interactive playground for experimenting with flex layout properties. Test alignItems, justifyContent, flexDirection, spacing, and padding with live visual feedback.',
- route: '/testing-grounds/flex-playground',
- },
- {
- id: 'chart-playground',
- title: 'Chart Playground',
- description:
- 'Explore all SwiftUI chart mark types: BarMark, LineMark, AreaMark, PointMark, RuleMark, and SectorMark. Randomize data to see animated transitions.',
- route: '/testing-grounds/chart-playground',
- },
- {
- id: 'gradient-playground',
- title: 'Gradient Playground',
- description:
- 'Test CSS gradient strings as backgroundColor. Experiment with linear, radial, and conic gradients, direction/angle controls, color presets, stop positions, and borderRadius clipping.',
- route: '/testing-grounds/gradient-playground',
- },
- {
- id: 'image-preloading',
- title: 'Image Preloading',
- description:
- 'Test image preloading functionality for Live Activities. Download images to App Group storage and verify they appear in Live Activities.',
- route: '/testing-grounds/image-preloading',
- },
- {
- id: 'image-fallback',
- title: 'Image Fallback',
- description:
- 'Explore the new image fallback behavior using backgroundColor from styles. Test missing images with various styling approaches.',
- route: '/testing-grounds/image-fallback',
- },
- {
- id: 'widget-scheduling',
- title: 'Widget Scheduling',
- description:
- 'Test widget timeline scheduling with multiple states. Configure timing for each state and watch widgets automatically transition between them.',
- route: '/testing-grounds/widget-scheduling',
- },
- {
- id: 'server-driven-widgets',
- title: 'Server-Driven Widgets',
- description:
- 'Test server-driven widget updates. Widgets fetch fresh content from a remote server without the user opening the app. Manage auth credentials and trigger reloads.',
- route: '/testing-grounds/server-driven-widgets',
- },
- ...(Platform.OS === 'ios'
- ? [
- {
- id: 'channel-updates',
- title: 'Channel-Based Updates',
- description: 'Start a minimal Live Activity bound to a broadcast channel ID and test server-driven updates.',
- route: '/testing-grounds/channel-updates',
- },
- ]
- : []),
- // Add more sections here as they are implemented
-]
+import AndroidOthersScreen from '~/screens/android/tabs/OthersScreen'
+import IOSOthersScreen from '~/screens/ios/tabs/OthersScreen'
export default function TestingGroundsScreen() {
- const router = useRouter()
-
- return (
-
-
- Testing Grounds
-
- Explore different aspects of Voltra development. Each section provides hands-on examples and demonstrations of
- specific features.
-
-
- {TESTING_GROUNDS_SECTIONS.map((section) => (
-
- {section.title}
- {section.description}
-
- router.push(section.route)} />
-
-
- ))}
-
-
- router.push('/live-activities')} />
-
-
-
- )
+ return Platform.OS === 'android' ? :
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
- buttonContainer: {
- marginTop: 16,
- },
- footer: {
- marginTop: 24,
- alignItems: 'center',
- },
-})
diff --git a/example/screens/testing-grounds/WidgetSchedulingScreen.tsx b/example/screens/testing-grounds/WidgetSchedulingScreen.tsx
index 7cffdabb..5b7d9557 100644
--- a/example/screens/testing-grounds/WidgetSchedulingScreen.tsx
+++ b/example/screens/testing-grounds/WidgetSchedulingScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { Alert, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { reloadWidgets, scheduleWidget, VoltraWidgetPreview } from 'voltra/client'
+import { Alert, StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { reloadWidgets, scheduleWidget, VoltraWidgetPreview } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
export default function WidgetSchedulingScreen() {
const colorScheme = useColorScheme()
@@ -190,192 +191,164 @@ export default function WidgetSchedulingScreen() {
}
return (
-
-
- Widget Scheduling
-
- Test widget timeline scheduling with multiple states. Configure when each state should appear and watch the
- widget transition automatically.
-
+
+ {/* Configuration */}
+
+ ⚙️ Configuration
+ Set when each future state should appear:
- {/* Configuration */}
-
- ⚙️ Configuration
- Set when each future state should appear:
+
+ State 2 (minutes from now):
+
+
-
- State 2 (minutes from now):
-
-
+
+ State 3 (minutes from now):
+
+
+
-
- State 3 (minutes from now):
-
-
-
+ {/* Schedule Timeline */}
+
+ 📅 Schedule Timeline
+
+ Schedules three widget states:{'\n\n'}• State 1 (Blue): Yesterday - shows as current{'\n'}• State 2 (Green):{' '}
+ {minutesUntilSecond || '2'} minutes from now{'\n'}• State 3 (Purple): {minutesUntilThird || '5'} minutes from
+ now{'\n\n'}
+ Add the widget to your home screen to see it transition between states.
+
+
+
+ {scheduledTimes && (
+
+ )}
+
+
- {/* Schedule Timeline */}
+ {/* Scheduled Times */}
+ {scheduledTimes && (
- 📅 Schedule Timeline
-
- Schedules three widget states:{'\n\n'}• State 1 (Blue): Yesterday - shows as current{'\n'}• State 2 (Green):{' '}
- {minutesUntilSecond || '2'} minutes from now{'\n'}• State 3 (Purple): {minutesUntilThird || '5'} minutes
- from now{'\n\n'}
- Add the widget to your home screen to see it transition between states.
-
-
-
- {scheduledTimes && (
-
- )}
-
-
-
- {/* Scheduled Times */}
- {scheduledTimes && (
-
- ⏰ Scheduled Times
-
-
-
-
- State 1 (Current)
- {scheduledTimes.past}
-
+ ⏰ Scheduled Times
+
+
+
+
+ State 1 (Current)
+ {scheduledTimes.past}
-
-
-
- State 2
- {scheduledTimes.second}
-
+
+
+
+
+ State 2
+ {scheduledTimes.second}
-
-
-
- State 3
- {scheduledTimes.third}
-
+
+
+
+
+ State 3
+ {scheduledTimes.third}
-
- )}
-
- {/* Previews */}
-
- Widget Previews
-
- State 1 (Current) - Blue
-
-
-
-
- STATE 1
- Current State
- Scheduled: Yesterday
-
-
-
+
+ )}
- State 2 - Green
-
-
-
-
- STATE 2
- Second State
-
- +{minutesUntilSecond || '2'} minutes
-
-
-
-
-
+ {/* Previews */}
+
+ Widget Previews
- State 3 - Purple
-
-
-
-
- STATE 3
- Third State
-
- +{minutesUntilThird || '5'} minutes
-
-
-
-
-
-
+ State 1 (Current) - Blue
+
+
+
+
+ STATE 1
+ Current State
+ Scheduled: Yesterday
+
+
+
+
- {/* How to Test */}
-
- 📝 How to Test
-
- 1. Configure the timing for states 2 and 3 above{'\n'}
- 2. Click Schedule Timeline{'\n'}
- 3. Add the Weather widget to your home screen{'\n'}
- 4. Verify it shows State 1 (blue background){'\n'}
- 5. Wait for the scheduled times{'\n'}
- 6. Watch the widget automatically transition:{'\n'}
- {' '}• State 1 (Blue) → State 2 (Green) → State 3 (Purple){'\n\n'}
- Note: iOS may delay widget updates based on battery level, widget
- visibility, and system load. For immediate updates during testing, keep Xcode attached or use shorter time
- intervals.
-
-
+ State 2 - Green
+
+
+
+
+ STATE 2
+ Second State
+
+ +{minutesUntilSecond || '2'} minutes
+
+
+
+
+
- {/* Back Button */}
-
- router.back()} />
+ State 3 - Purple
+
+
+
+
+ STATE 3
+ Third State
+
+ +{minutesUntilThird || '5'} minutes
+
+
+
+
-
-
+
+
+ {/* How to Test */}
+
+ 📝 How to Test
+
+ 1. Configure the timing for states 2 and 3 above{'\n'}
+ 2. Click Schedule Timeline{'\n'}
+ 3. Add the Weather widget to your home screen{'\n'}
+ 4. Verify it shows State 1 (blue background){'\n'}
+ 5. Wait for the scheduled times{'\n'}
+ 6. Watch the widget automatically transition:{'\n'}
+ {' '}• State 1 (Blue) → State 2 (Green) → State 3 (Purple){'\n\n'}
+ Note: iOS may delay widget updates based on battery level, widget visibility,
+ and system load. For immediate updates during testing, keep Xcode attached or use shorter time intervals.
+
+
+
+ {/* Back Button */}
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 24,
- },
bold: {
fontWeight: '700',
color: '#FFFFFF',
diff --git a/example/screens/testing-grounds/chart-playground/ChartPlaygroundScreen.tsx b/example/screens/testing-grounds/chart-playground/ChartPlaygroundScreen.tsx
index 73274b51..77da386f 100644
--- a/example/screens/testing-grounds/chart-playground/ChartPlaygroundScreen.tsx
+++ b/example/screens/testing-grounds/chart-playground/ChartPlaygroundScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React, { useCallback, useState } from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { StyleSheet, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
// ─── data helpers ───────────────────────────────────────────────────────────
@@ -50,6 +51,7 @@ function ChartPreview({ children }: { children: React.ReactNode }) {
// ─── screen ─────────────────────────────────────────────────────────────────
export default function ChartPlaygroundScreen() {
+ const router = useRouter()
const [barData, setBarData] = useState(randomBarData)
const [multiData, setMultiData] = useState(randomMultiSeriesData)
const [lineData, setLineData] = useState(randomLineData)
@@ -79,252 +81,218 @@ export default function ChartPlaygroundScreen() {
}, [])
return (
-
-
- Chart Playground
-
- All SwiftUI chart mark types powered by Voltra. Tap Randomize to animate between data sets.
-
+
+
+
+
-
-
+
+ BarMark
+ Single series bar chart with rounded corners.
+
+ setBarData(randomBarData())} variant="secondary" />
+
+
+
+
+
+
- {/* BarMark */}
-
- BarMark
- Single series bar chart with rounded corners.
-
- setBarData(randomBarData())} variant="secondary" />
-
-
-
-
-
-
-
-
- {/* BarMark multi-series */}
-
- BarMark — Multi-series
-
- Two series (A & B) rendered as grouped bars using the supported `stacking` grouped mode.
-
-
- setMultiData(randomMultiSeriesData())} variant="secondary" />
-
-
-
-
-
-
-
+ {/* BarMark multi-series */}
+
+ BarMark — Multi-series
+ Two series (A & B) rendered as grouped bars using the supported `stacking` grouped mode.
+
+ setMultiData(randomMultiSeriesData())} variant="secondary" />
+
+
+
+
+
+
+
- {/* LineMark */}
-
- LineMark
- Smooth monotone line chart.
-
- setLineData(randomLineData())} variant="secondary" />
-
-
-
-
-
-
-
+ {/* LineMark */}
+
+ LineMark
+ Smooth monotone line chart.
+
+ setLineData(randomLineData())} variant="secondary" />
+
+
+
+
+
+
+
- {/* AreaMark */}
-
- AreaMark
- Filled area chart — the classic stocks-app look.
-
- setAreaData(randomAreaData())} variant="secondary" />
-
-
-
-
-
-
-
+ {/* AreaMark */}
+
+ AreaMark
+ Filled area chart — the classic stocks-app look.
+
+ setAreaData(randomAreaData())} variant="secondary" />
+
+
+
+
+
+
+
- {/* PointMark */}
-
- PointMark
-
- Scatter plot with numeric x and y axes plus both vertical and horizontal reference lines.
-
-
- {
- setPointData(randomPointData())
- setPointRuleY(randomPointRuleY())
- setPointRuleX(randomPointRuleX())
- }}
- variant="secondary"
- />
-
-
-
-
-
-
-
-
+ {/* PointMark */}
+
+ PointMark
+ Scatter plot with numeric x and y axes plus both vertical and horizontal reference lines.
+
+ {
+ setPointData(randomPointData())
+ setPointRuleY(randomPointRuleY())
+ setPointRuleX(randomPointRuleX())
+ }}
+ variant="secondary"
+ />
+
+
+
+
+
+
+
+
- {/* RuleMark */}
-
- RuleMark
-
- Bar chart with both horizontal and vertical reference lines. When both `xValue` and `yValue` are set, both
- lines render.
-
-
- {
- setBarData(randomBarData())
- setRuleY(randomRuleY())
- setRuleX(randomRuleX())
- }}
- variant="secondary"
- />
-
-
-
-
-
-
-
-
+ {/* RuleMark */}
+
+ RuleMark
+
+ Bar chart with both horizontal and vertical reference lines. When both `xValue` and `yValue` are set, both
+ lines render.
+
+
+ {
+ setBarData(randomBarData())
+ setRuleY(randomRuleY())
+ setRuleX(randomRuleX())
+ }}
+ variant="secondary"
+ />
+
+
+
+
+
+
+
+
- {/* SectorMark — pie */}
-
- SectorMark — Pie
- Pie chart built with SectorMark (iOS 17+).
-
- setSectorData(randomSectorData())} variant="secondary" />
-
-
-
-
-
-
-
+ {/* SectorMark — pie */}
+
+ SectorMark — Pie
+ Pie chart built with SectorMark (iOS 17+).
+
+ setSectorData(randomSectorData())} variant="secondary" />
+
+
+
+
+
+
+
- {/* SectorMark — donut */}
-
- SectorMark — Donut
- Same data as above but with an inner radius to create a donut chart.
-
- setSectorData(randomSectorData())} variant="secondary" />
-
-
-
-
-
-
-
+ {/* SectorMark — donut */}
+
+ SectorMark — Donut
+ Same data as above but with an inner radius to create a donut chart.
+
+ setSectorData(randomSectorData())} variant="secondary" />
+
+
+
+
+
+
+
- {/* Combo: Bar + Line */}
-
- Combo — Bar + Line
- Multiple mark types composited in one chart.
-
- {
- setComboBarData(randomBarData())
- setComboLineData(randomLineData())
- }}
- variant="secondary"
- />
-
-
-
-
-
-
-
-
+ {/* Combo: Bar + Line */}
+
+ Combo — Bar + Line
+ Multiple mark types composited in one chart.
+
+ {
+ setComboBarData(randomBarData())
+ setComboLineData(randomLineData())
+ }}
+ variant="secondary"
+ />
+
+
+
+
+
+
+
+
- {/* Axis visibility */}
-
- Hidden Axes
- Chart with both axes hidden — clean minimal look.
-
-
-
-
-
-
+ {/* Axis visibility */}
+
+ Hidden Axes
+ Chart with both axes hidden — clean minimal look.
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- backgroundColor: '#0F172A',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 16,
- },
randomizeRow: {
marginBottom: 8,
},
diff --git a/example/screens/testing-grounds/components/ComponentsScreen.tsx b/example/screens/testing-grounds/components/ComponentsScreen.tsx
index 6eb06fde..d57c661b 100644
--- a/example/screens/testing-grounds/components/ComponentsScreen.tsx
+++ b/example/screens/testing-grounds/components/ComponentsScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React from 'react'
-import { FlatList, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { StyleSheet, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
const IMAGE =
'/9j/4AAQSkZJRgABAQAASABIAAD/4QCARXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAIAAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/AABEIAIAAgAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2wBDAAICAgICAgMCAgMFAwMDBQYFBQUFBggGBgYGBggKCAgICAgICgoKCgoKCgoMDAwMDAwODg4ODg8PDw8PDw8PDw//2wBDAQICAgQEBAcEBAcQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/3QAEAAj/2gAMAwEAAhEDEQA/APyi5z0o5pMc0Y5NapgHPNHNFGO9aKQDu3NLzTfxpa7qEykx/NPAOaZTgOa9eiyh4Bp6g00DmpFFenSiaIkVTVlFqFBVqMV62HpmiRNGtW1TpiooxxirajpXv4ekrG8EKENDJUoApGFeh7JWNbH/0PyhAoxxR3oFaAHajjHWiigA7UopOMUtdFKWoDx9af361H3qQda9vCTuaJki9alUc1Ggy2M1Oqqeh7V9BQiaJEqCrSAYqqnSradK9jDRNYlpOlW1qqgq0McV72HTOiBKPrSE880UN1ruLP/R/KHjNHFAIB4o49K0AOKSl/Ck7UALxRRxijjFXABwxUi9ajqyFQMOP1r2sErsuKJkCbhwT+NPV17DqPX/AOtUQcZOBSp619RSlpobXLKYq3HjFU0PFW0r2MKzWJcjxVtaqx4q2vSvocMdER/FDdafxTW612uOho0f/9L8oR1ooGc0ZrVoA5xR26UA8UmeKQC80UGrKZ2DA+vFb0KfMxpAAABwOg7UrSMrkDHB9Ke24ng44HYUnlZOSTz7V7tGD2iapMRSakU805YvU/pTxHgE5zXv0KUki0mPTNWozVNc9cVYRq9XDyNEzRjbirStWfG1WlY8V7+Hq2N4suBqa7c1Fv4pGfnrXa6ysacx/9P8osUmKdjmkxXdKkwE5opeaTHFZumAYxTufpSY4pcGt6cAJxLgAEZwPWpRMPQ/nVWnY5r2sNKRqmy2suTgKSasoWLYZCvHWq8ULhwWwAD61OzJtYbhyD3r6LDcyV5m0fMkkP7s1GrGqoJ61Ip4rdV7u4c1y6rVOHPFZ4bipN/Su2nirFqRf8w00uap7/ekLkZrV41D5j//1Pykx70YFO4zijivonQNLDMUYpwHWlwMVP1YXKNwMUY4p2BijjitIUB2DAq2ts/ByKFttyht3Uen/wBep2njRtpBJXjp6fjXt4bCKKvUNIx7i+bFyN3r2qiMCkyCeKSqq4hyFz3Hg++adn3qHIHejIrmlirC5iff70eYBVbPFNz3zXLPMn0FzlnzfelMoz7VVzSE+9c7zKW1xc5//9X8px16UCgHB5pfwr6+KuaCetHalFHtWqiAUcUfhS54HFXGAFpbkKoXbnAxVd23uzY6nNN70E1vUrSasynJvcSkyO9GaZmvNr1+UkM0meOlJnrSHpzXkVKz3IuLmgmkozx0rllUEBozzSZpeM1HOB//1vynHWlpBnNLzX2EDQBS9qOaTJrdALnijNHOKXnirQCGkPWlJNIc1nVkAwmm0uTTea8KvO7JkJmgdKOcUc45rgkyQoz7Uc0HNYsANHejmjJzQB//2Q=='
@@ -296,69 +297,29 @@ const COMPONENTS_DATA = [
]
export default function ComponentsScreen() {
- const renderHeader = () => (
- <>
- Components Showcase
-
- Explore all available Voltra components. Each example demonstrates the component's functionality and
- styling capabilities within Live Activities.
-
- >
- )
-
- const renderItem = ({ item }: { item: (typeof COMPONENTS_DATA)[0] }) => (
-
- {item.title}
- {item.description}
- {item.renderExample()}
-
- )
-
- const renderFooter = () => (
-
-
-
-
-
- )
+ const router = useRouter()
return (
-
- item.id}
- ListHeaderComponent={renderHeader}
- renderItem={renderItem}
- ListFooterComponent={renderFooter}
- />
-
+
+ {COMPONENTS_DATA.map((item) => (
+
+ {item.title}
+ {item.description}
+ {item.renderExample()}
+
+ ))}
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
footer: {
marginTop: 24,
alignItems: 'center',
diff --git a/example/screens/testing-grounds/flex-playground/FlexPlaygroundScreen.tsx b/example/screens/testing-grounds/flex-playground/FlexPlaygroundScreen.tsx
index 72d90bc4..ba3fe7d0 100644
--- a/example/screens/testing-grounds/flex-playground/FlexPlaygroundScreen.tsx
+++ b/example/screens/testing-grounds/flex-playground/FlexPlaygroundScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { StyleSheet, Text, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
type FlexDirection = 'row' | 'column'
type AlignItems = 'flex-start' | 'center' | 'flex-end' | 'stretch'
@@ -44,6 +45,7 @@ const JUSTIFY_CONTENT_LABELS: Record = {
}
export default function FlexPlaygroundScreen() {
+ const router = useRouter()
const [flexDirection, setFlexDirection] = useState('column')
const [alignItems, setAlignItems] = useState('stretch')
const [justifyContent, setJustifyContent] = useState('flex-start')
@@ -75,178 +77,140 @@ export default function FlexPlaygroundScreen() {
const decreasePadding = () => setContainerPadding((prev) => Math.max(prev - 4, 0))
return (
-
-
- Flex Layout Playground
-
- Experiment with flex properties using the new View component with dynamic flexDirection.
-
-
- {/* Controls */}
-
- Controls
-
- {/* Flex Direction */}
-
- Flex Direction:
-
-
+
+
+ Controls
+
+
+ Flex Direction:
+
+
- {/* Align Items */}
-
- Align Items:
-
-
+
+ Align Items:
+
+
- {/* Justify Content */}
-
- Justify Content:
-
-
+
+ Justify Content:
+
+
- {/* Gap */}
-
- Gap: {gap}px
-
-
-
-
+
+ Gap: {gap}px
+
+
+
+
- {/* Container Padding */}
-
- Padding: {containerPadding}px
-
-
-
-
+
+ Padding: {containerPadding}px
+
+
+
-
-
- {/* Preview */}
-
- Live Preview
- See how your flex settings affect the layout below
-
-
+
+
+
+
+ Live Preview
+ See how your flex settings affect the layout below
+
+
+
-
- Item 1
-
-
-
- Item 2
-
-
-
- Item 3
-
+ Item 1
-
-
-
- {/* Text Align Test */}
-
- Text Align in Flex
- Text alignment within stretched flex children
-
-
- textAlign: left
-
-
-
- textAlign: center
-
-
-
-
- textAlign: right
-
-
+ Item 2
-
-
-
-
-
-
-
-
-
+
+ Item 3
+
+
+
+
+
+
+ Text Align in Flex
+ Text alignment within stretched flex children
+
+
+
+
+ textAlign: left
+
+
+
+ textAlign: center
+
+
+
+ textAlign: right
+
+
+
+
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 20,
- },
controlRow: {
flexDirection: 'row',
justifyContent: 'space-between',
diff --git a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx
index 5d97858f..35bd5b1f 100644
--- a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx
+++ b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { ScrollView, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { StyleSheet, Text, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
type GradientType = 'linear' | 'radial' | 'conic'
type Direction = 'to right' | 'to bottom' | 'to bottom right' | 'to top right'
@@ -29,6 +30,7 @@ const PRESETS: { label: string; colors: [string, string, ...string[]] }[] = [
const ANGLE_OPTIONS = [0, 45, 90, 135, 180]
export default function GradientPlaygroundScreen() {
+ const router = useRouter()
const [gradientType, setGradientType] = useState('linear')
const [direction, setDirection] = useState('to right')
const [angle, setAngle] = useState(90)
@@ -81,185 +83,160 @@ export default function GradientPlaygroundScreen() {
const decreaseBorderRadius = () => setBorderRadius((prev) => Math.max(prev - 8, 0))
return (
-
-
- Gradient Playground
- Test CSS gradient strings as backgroundColor on Voltra views.
-
- Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug
- in iOS rendering.
-
-
- {/* Controls */}
-
- Controls
+
+
+ Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug
+ in iOS rendering.
+
- {/* Gradient Type */}
-
- Type:
-
-
+ {/* Controls */}
+
+ Controls
- {/* Direction (linear only) */}
- {gradientType === 'linear' && (
- <>
-
- Mode:
- setUseAngle((v) => !v)}
- variant="secondary"
- />
-
- {useAngle ? (
-
- Angle: {angle}deg
-
-
- ) : (
-
- Direction:
-
-
- )}
- >
- )}
+ {/* Gradient Type */}
+
+ Type:
+
+
- {/* Angle for conic */}
- {gradientType === 'conic' && (
+ {/* Direction (linear only) */}
+ {gradientType === 'linear' && (
+ <>
- Start angle: {angle}deg
-
+ Mode:
+ setUseAngle((v) => !v)}
+ variant="secondary"
+ />
- )}
+ {useAngle ? (
+
+ Angle: {angle}deg
+
+
+ ) : (
+
+ Direction:
+
+
+ )}
+ >
+ )}
- {/* Color Preset */}
+ {/* Angle for conic */}
+ {gradientType === 'conic' && (
- Colors: {PRESETS[preset].label}
-
+ Start angle: {angle}deg
+
+ )}
- {/* Border Radius */}
-
- borderRadius: {borderRadius}px
-
-
-
-
+ {/* Color Preset */}
+
+ Colors: {PRESETS[preset].label}
+
+
+
+ {/* Border Radius */}
+
+ borderRadius: {borderRadius}px
+
+
+
-
+
+
- {/* Live Preview */}
-
- Live Preview
+ {/* Live Preview */}
+
+ Live Preview
-
-
- Gradient View
- {gradient}
-
-
-
+
+
+ Gradient View
+ {gradient}
+
+
+
- {/* Explicit stop positions */}
-
- Color Stop Positions
- Explicit percentage stops: red 10%, yellow 50%, blue 90%
+ {/* Explicit stop positions */}
+
+ Color Stop Positions
+ Explicit percentage stops: red 10%, yellow 50%, blue 90%
-
-
-
-
+
+
+
+
- {/* rgba inside gradient */}
-
- RGBA Inside Gradient
-
- linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)
-
+ {/* rgba inside gradient */}
+
+ RGBA Inside Gradient
+
+ linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)
+
-
-
-
-
+
+
+
+
- {/* Solid color still works */}
-
- Solid Color (Unchanged)
- backgroundColor: "#3B82F6" — plain colors still work
+ {/* Solid color still works */}
+
+ Solid Color (Unchanged)
+ backgroundColor: "#3B82F6" — plain colors still work
-
-
- Solid #3B82F6
-
-
-
+
+
+ Solid #3B82F6
+
+
+
-
-
-
-
-
-
-
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
warningText: {
fontSize: 12,
lineHeight: 18,
diff --git a/example/screens/testing-grounds/positioning/PositioningExamples.tsx b/example/screens/testing-grounds/positioning/PositioningExamples.tsx
index e2ddaea9..9bb07a91 100644
--- a/example/screens/testing-grounds/positioning/PositioningExamples.tsx
+++ b/example/screens/testing-grounds/positioning/PositioningExamples.tsx
@@ -1,6 +1,6 @@
import React from 'react'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
export type PositioningExampleProps = {
testID?: string
diff --git a/example/screens/testing-grounds/positioning/PositioningScreen.tsx b/example/screens/testing-grounds/positioning/PositioningScreen.tsx
index c62fd2ed..05fc97ec 100644
--- a/example/screens/testing-grounds/positioning/PositioningScreen.tsx
+++ b/example/screens/testing-grounds/positioning/PositioningScreen.tsx
@@ -1,9 +1,10 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React from 'react'
-import { FlatList, StyleSheet, Text, View } from 'react-native'
+import { StyleSheet, View } from 'react-native'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import {
AbsolutePositioningBasicExample,
@@ -63,72 +64,32 @@ const POSITIONING_DATA = [
]
export default function PositioningScreen() {
- const renderHeader = () => (
- <>
- Positioning Examples
-
- Explore Voltra's positioning modes: static (default), relative (offset from natural position), and absolute
- (center-based coordinates). Red dots mark reference points in absolute positioning examples.
-
- >
- )
-
- const renderItem = ({ item }: { item: (typeof POSITIONING_DATA)[0] }) => {
- const { Component } = item
- return (
-
- {item.title}
- {item.description}
-
-
- )
- }
-
- const renderFooter = () => (
-
-
-
-
-
- )
+ const router = useRouter()
return (
-
- item.id}
- ListHeaderComponent={renderHeader}
- renderItem={renderItem}
- ListFooterComponent={renderFooter}
- />
-
+
+ {POSITIONING_DATA.map((item) => {
+ const { Component } = item
+ return (
+
+ {item.title}
+ {item.description}
+
+
+ )
+ })}
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
footer: {
marginTop: 24,
alignItems: 'center',
diff --git a/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx b/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx
index 42957cce..abf60d94 100644
--- a/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx
+++ b/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { ScrollView, StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraWidgetPreview } from 'voltra/client'
+import { StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraWidgetPreview } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
export default function ProgressTestingScreen() {
const router = useRouter()
@@ -103,232 +104,223 @@ export default function ProgressTestingScreen() {
}
return (
-
-
- Progress Testing
-
- Test VoltraLinearProgressView and VoltraCircularProgressView with new label and styling support.
-
+
+ {/* 1. Live Preview */}
+
+ Live Preview
+
+
+ {renderProgressWidget()}
+
+
+ {mode === 'timer' && (
+
+ )}
+
+
+ {/* 2. Configuration */}
+
+ Base Configuration
- {/* 1. Live Preview */}
-
- Live Preview
-
-
- {renderProgressWidget()}
-
+
+ Type
+
+ setType('linear')}
+ style={styles.smButton}
+ />
+ setType('circular')}
+ style={styles.smButton}
+ />
- {mode === 'timer' && (
-
- )}
-
+
- {/* 2. Configuration */}
-
- Base Configuration
+
+ Mode
+
+ setMode('determinate')}
+ style={styles.smButton}
+ />
+ setMode('timer')}
+ style={styles.smButton}
+ />
+ setMode('indeterminate')}
+ style={styles.smButton}
+ />
+
+
+ {mode === 'determinate' && (
- Type
+ Progress: {progressValue}%
setType('linear')}
+ title="-10"
+ variant="secondary"
+ onPress={() => setProgressValue(Math.max(0, progressValue - 10))}
style={styles.smButton}
/>
setType('circular')}
+ title="+10"
+ variant="secondary"
+ onPress={() => setProgressValue(Math.min(100, progressValue + 10))}
style={styles.smButton}
/>
+ )}
-
- Mode
-
- setMode('determinate')}
- style={styles.smButton}
- />
- setMode('timer')}
- style={styles.smButton}
+ {mode === 'timer' && (
+ <>
+
+ Duration (seconds)
+
+
+
+ Count Down
setMode('indeterminate')}
+ title={countDown ? 'ON' : 'OFF'}
+ variant={countDown ? 'primary' : 'secondary'}
+ onPress={() => setCountDown(!countDown)}
style={styles.smButton}
/>
-
+ Note: Custom styling is ignored for Timers to support realtime updates.
+ >
+ )}
+
+
+ {/* 3. Styling Configuration */}
+
+ Styling Configuration
+
+
+ Track Color
+
+
+
+
+ Progress Color
+
+
- {mode === 'determinate' && (
+ {type === 'linear' ? (
+ <>
- Progress: {progressValue}%
+ Height: {height}
setProgressValue(Math.max(0, progressValue - 10))}
+ title="Small"
+ variant={height === 4 ? 'primary' : 'secondary'}
+ onPress={() => setHeight(4)}
style={styles.smButton}
/>
setProgressValue(Math.min(100, progressValue + 10))}
+ title="Medium"
+ variant={height === 8 ? 'primary' : 'secondary'}
+ onPress={() => setHeight(8)}
style={styles.smButton}
/>
-
-
- )}
-
- {mode === 'timer' && (
- <>
-
- Duration (seconds)
-
-
-
- Count Down
setCountDown(!countDown)}
+ title="Large"
+ variant={height === 16 ? 'primary' : 'secondary'}
+ onPress={() => setHeight(16)}
style={styles.smButton}
/>
- Note: Custom styling is ignored for Timers to support realtime updates.
- >
- )}
-
-
- {/* 3. Styling Configuration */}
-
- Styling Configuration
-
-
- Track Color
-
-
-
-
- Progress Color
-
-
-
- {type === 'linear' ? (
- <>
-
- Height: {height}
-
- setHeight(4)}
- style={styles.smButton}
- />
- setHeight(8)}
- style={styles.smButton}
- />
- setHeight(16)}
- style={styles.smButton}
- />
-
-
-
- Corner Radius: {cornerRadius}
-
- setCornerRadius(0)}
- style={styles.smButton}
- />
- setCornerRadius(4)}
- style={styles.smButton}
- />
- setCornerRadius(20)}
- style={styles.smButton}
- />
-
-
-
- Custom Thumb
- setUseThumb(!useThumb)}
- style={styles.smButton}
- />
-
- >
- ) : (
+
- Line Width: {lineWidth}
+ Corner Radius: {cornerRadius}
setLineWidth(2)}
+ title="None"
+ variant={cornerRadius === 0 ? 'primary' : 'secondary'}
+ onPress={() => setCornerRadius(0)}
style={styles.smButton}
/>
setLineWidth(6)}
+ title="Small"
+ variant={cornerRadius === 4 ? 'primary' : 'secondary'}
+ onPress={() => setCornerRadius(4)}
style={styles.smButton}
/>
setLineWidth(12)}
+ title="Full"
+ variant={cornerRadius === 20 ? 'primary' : 'secondary'}
+ onPress={() => setCornerRadius(20)}
style={styles.smButton}
/>
- )}
-
+
+ Custom Thumb
+ setUseThumb(!useThumb)}
+ style={styles.smButton}
+ />
+
+ >
+ ) : (
+
+ Line Width: {lineWidth}
+
+ setLineWidth(2)}
+ style={styles.smButton}
+ />
+ setLineWidth(6)}
+ style={styles.smButton}
+ />
+ setLineWidth(12)}
+ style={styles.smButton}
+ />
+
+
+ )}
+
-
- router.back()} />
-
-
-
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: { flex: 1 },
- scrollView: { flex: 1 },
- content: { padding: 20 },
- heading: { fontSize: 24, fontWeight: '700', color: '#FFFFFF', marginBottom: 8 },
- subheading: { fontSize: 14, color: '#CBD5F5', marginBottom: 24 },
previewContainer: { flexDirection: 'row', justifyContent: 'center', flexWrap: 'wrap', gap: 12, padding: 10 },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
label: { color: '#fff', fontSize: 16 },
diff --git a/example/screens/testing-grounds/styling/StylingScreen.tsx b/example/screens/testing-grounds/styling/StylingScreen.tsx
index 8736c323..dc36f82b 100644
--- a/example/screens/testing-grounds/styling/StylingScreen.tsx
+++ b/example/screens/testing-grounds/styling/StylingScreen.tsx
@@ -1,11 +1,12 @@
-import { Link } from 'expo-router'
+import { useRouter } from 'expo-router'
import React from 'react'
-import { FlatList, StyleSheet, Text, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraView } from 'voltra/client'
+import { StyleSheet, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraView } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
const STYLING_DATA = [
{
@@ -250,69 +251,29 @@ const STYLING_DATA = [
]
export default function StylingScreen() {
- const renderHeader = () => (
- <>
- Styling Examples
-
- Explore Voltra's styling capabilities. Each example demonstrates different styling properties that can be
- applied to Voltra components.
-
- >
- )
-
- const renderItem = ({ item }: { item: (typeof STYLING_DATA)[0] }) => (
-
- {item.title}
- {item.description}
- {item.renderExample()}
-
- )
-
- const renderFooter = () => (
-
-
-
-
-
- )
+ const router = useRouter()
return (
-
- item.id}
- ListHeaderComponent={renderHeader}
- renderItem={renderItem}
- ListFooterComponent={renderFooter}
- />
-
+
+ {STYLING_DATA.map((item) => (
+
+ {item.title}
+ {item.description}
+ {item.renderExample()}
+
+ ))}
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 8,
- },
footer: {
marginTop: 24,
alignItems: 'center',
diff --git a/example/screens/testing-grounds/timer/TimerTestingScreen.tsx b/example/screens/testing-grounds/timer/TimerTestingScreen.tsx
index bac86cd3..2523352e 100644
--- a/example/screens/testing-grounds/timer/TimerTestingScreen.tsx
+++ b/example/screens/testing-grounds/timer/TimerTestingScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
-import { ScrollView, StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { VoltraWidgetPreview } from 'voltra/client'
+import { StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
+import { Voltra } from '@use-voltra/ios'
+import { VoltraWidgetPreview } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
const DEFAULT_TEMPLATES = {
running: 'Time remaining: {time}',
@@ -86,139 +87,130 @@ export default function TimerTestingScreen() {
)
return (
-
-
- Timer Testing
-
- Test the VoltraTimer component behaviors, including native text updates for Live Activities.
-
-
- {/* Preview */}
-
- Live Preview
-
-
- {renderTimerWidget()}
-
-
-
-
-
- {/* Configuration */}
-
- Configuration
-
-
- Mode
-
- setMode('timer')}
- style={styles.smButton}
- />
- setMode('stopwatch')}
- style={styles.smButton}
- />
-
+
+ {/* Preview */}
+
+ Live Preview
+
+
+ {renderTimerWidget()}
+
+
+
+
+
+ {/* Configuration */}
+
+ Configuration
+
+
+ Mode
+
+ setMode('timer')}
+ style={styles.smButton}
+ />
+ setMode('stopwatch')}
+ style={styles.smButton}
+ />
+
- {mode === 'timer' && (
- <>
-
- Direction
-
- setDirection('down')}
- style={styles.smButton}
- />
- setDirection('up')}
- style={styles.smButton}
- />
-
-
-
-
- Duration (seconds)
-
+
+ Direction
+
+ setDirection('down')}
+ style={styles.smButton}
+ />
+ setDirection('up')}
+ style={styles.smButton}
/>
- >
- )}
-
-
- Style
-
- setTextStyle('timer')}
- style={styles.smButton}
- />
- setTextStyle('relative')}
- style={styles.smButton}
- />
-
-
- Show Hours
-
- setShowHours(false)}
- style={styles.smButton}
- />
- setShowHours(true)}
- style={styles.smButton}
+
+ Duration (seconds)
+
+ >
+ )}
+
+
+ Style
+
+ setTextStyle('timer')}
+ style={styles.smButton}
+ />
+ setTextStyle('relative')}
+ style={styles.smButton}
+ />
+
-
- Templates (JSON)
-
+ Show Hours
+
+ setShowHours(false)}
+ style={styles.smButton}
+ />
+ setShowHours(true)}
+ style={styles.smButton}
/>
-
+
-
- router.back()} />
+
+ Templates (JSON)
+
-
-
+
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: { flex: 1 },
- scrollView: { flex: 1 },
- content: { padding: 20 },
- heading: { fontSize: 24, fontWeight: '700', color: '#FFFFFF', marginBottom: 8 },
- subheading: { fontSize: 14, color: '#CBD5F5', marginBottom: 24 },
previewContainer: { alignItems: 'center', padding: 10 },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
col: { marginBottom: 16 },
diff --git a/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx b/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
index 7828455a..613966cd 100644
--- a/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
+++ b/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
@@ -1,11 +1,12 @@
import { useRouter } from 'expo-router'
import React, { useEffect, useState } from 'react'
import { Alert, ScrollView, StyleSheet, Text, useColorScheme, View } from 'react-native'
-import { Voltra } from 'voltra'
-import { reloadWidgets, scheduleWidget, updateWidget, VoltraWidgetPreview, WidgetFamily } from 'voltra/client'
+import { Voltra } from '@use-voltra/ios'
+import { reloadWidgets, scheduleWidget, updateWidget, VoltraWidgetPreview, WidgetFamily } from '@use-voltra/ios-client'
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
+import { ScreenLayout } from '~/components/ScreenLayout'
import { IosWeatherWidget } from '~/widgets/ios/IosWeatherWidget'
import { SAMPLE_WEATHER_DATA, type WeatherCondition, type WeatherData } from '~/widgets/weather-types'
@@ -250,153 +251,118 @@ export default function WeatherTestingScreen() {
}, [])
return (
-
-
- Weather Widget Testing
-
- Test the weather widget with different conditions and widget sizes. Choose from Sunny, Cloudy, or Rainy
- weather with beautiful gradient backgrounds.
-
+
+
+ Current Weather: {WEATHER_CONDITIONS.find((c) => c.id === selectedWeather)?.label}
+
+ Temperature: {currentWeather.temperature}°F
+ {currentWeather.highTemp && currentWeather.lowTemp ? (
+ <>
+ {' '}
+ • High: {currentWeather.highTemp}° • Low: {currentWeather.lowTemp}°
+ >
+ ) : null}
+ {currentWeather.location ? <> • {currentWeather.location}> : null}
+
+
- {/* Current Weather Display */}
-
- Current Weather: {WEATHER_CONDITIONS.find((c) => c.id === selectedWeather)?.label}
-
- Temperature: {currentWeather.temperature}°F
- {currentWeather.highTemp && currentWeather.lowTemp ? (
- <>
- {' '}
- • High: {currentWeather.highTemp}° • Low: {currentWeather.lowTemp}°
- >
- ) : null}
- {currentWeather.location ? <> • {currentWeather.location}> : null}
-
-
-
- {/* Widget Family Selection */}
-
- Widget Family: {WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.title}
- {WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.description}
-
-
- {WIDGET_FAMILIES.map((family) => (
- setSelectedFamily(family.id)}
- style={styles.familyButton}
- />
- ))}
-
-
-
-
- {/* Weather Condition Buttons */}
-
- Weather Conditions
- Select a weather condition to update the widget:
-
- {WEATHER_CONDITIONS.map((condition) => (
+
+ Widget Family: {WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.title}
+ {WIDGET_FAMILIES.find((f) => f.id === selectedFamily)?.description}
+
+
+ {WIDGET_FAMILIES.map((family) => (
handleWeatherChange(condition.id)}
- style={styles.weatherButton}
- disabled={isUpdating}
+ key={family.id}
+ title={family.title}
+ variant={selectedFamily === family.id ? 'primary' : 'secondary'}
+ onPress={() => setSelectedFamily(family.id)}
+ style={styles.familyButton}
/>
))}
-
-
- {/* Quick Actions */}
-
- Quick Actions
-
-
-
-
-
+
+
- {/* Timeline Scheduling */}
-
- 📅 Timeline Scheduling
-
- Schedule multiple weather updates in advance. iOS will automatically display each forecast at the scheduled
- time, even when the app is closed.
-
-
-
- Schedules 4 entries: 1 (+5sec), 2 (+1min), 3 (+2min), 4 (+3min). Each has a different background color.
- Note: iOS may delay updates based on battery/visibility. Test with Xcode attached for immediate updates.
-
-
+
+ Weather Conditions
+ Select a weather condition to update the widget:
+
+ {WEATHER_CONDITIONS.map((condition) => (
+ handleWeatherChange(condition.id)}
+ style={styles.weatherButton}
+ disabled={isUpdating}
+ />
+ ))}
+
+
- {/* Widget Preview */}
-
- Widget Preview
-
- This shows how the weather widget will appear on your home screen. The widget updates in real-time when you
- change the weather condition above.
-
-
-
-
-
-
-
+
+ Quick Actions
+
+
+
+
+
- {/* Instructions */}
-
- How to Test
-
- 1. Select a widget family (size) above{'\n'}
- 2. Choose different weather conditions (Sunny, Cloudy, Rainy){'\n'}
- 3. Notice how the gradient background changes{'\n'}
- 4. Check your home screen to see the live widget update{'\n'}
- 5. Try the random weather button for variety
-
-
+
+ 📅 Timeline Scheduling
+
+ Schedule multiple weather updates in advance. iOS will automatically display each forecast at the scheduled
+ time, even when the app is closed.
+
+
+
+ Schedules 4 entries: 1 (+5sec), 2 (+1min), 3 (+2min), 4 (+3min). Each has a different background color. Note:
+ iOS may delay updates based on battery/visibility. Test with Xcode attached for immediate updates.
+
+
- {/* Back Button */}
-
- router.back()} />
+
+ Widget Preview
+
+ This shows how the weather widget will appear on your home screen. The widget updates in real-time when you
+ change the weather condition above.
+
+
+
+
+
-
-
+
+
+
+ How to Test
+
+ 1. Select a widget family (size) above{'\n'}
+ 2. Choose different weather conditions (Sunny, Cloudy, Rainy){'\n'}
+ 3. Notice how the gradient background changes{'\n'}
+ 4. Check your home screen to see the live widget update{'\n'}
+ 5. Try the random weather button for variety
+
+
+
+
+ router.back()} />
+
+
)
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- paddingHorizontal: 20,
- paddingVertical: 24,
- },
- heading: {
- fontSize: 24,
- fontWeight: '700',
- color: '#FFFFFF',
- marginBottom: 8,
- },
- subheading: {
- fontSize: 14,
- lineHeight: 20,
- color: '#CBD5F5',
- marginBottom: 24,
- },
weatherButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
diff --git a/example/server/widget-server.tsx b/example/server/widget-server.tsx
index e0460a61..043b58c8 100644
--- a/example/server/widget-server.tsx
+++ b/example/server/widget-server.tsx
@@ -8,8 +8,10 @@
*/
import { createServer } from 'node:http'
+import { renderAndroidWidgetToString } from '@use-voltra/android-server'
+import { renderWidgetToString } from '@use-voltra/ios-server'
+import { createWidgetUpdateNodeHandler } from '@use-voltra/server'
import React from 'react'
-import { createWidgetUpdateNodeHandler } from 'voltra/server'
import { IosPortfolioWidget } from '../widgets/ios/IosPortfolioWidget'
import { AndroidMaterialColorsServerWidget } from '../widgets/android/AndroidMaterialColorsWidget'
import { AndroidPortfolioWidget } from '../widgets/android/AndroidPortfolioWidget'
@@ -60,12 +62,13 @@ const handler = createWidgetUpdateNodeHandler({
console.log(`[${now}] [iOS] Rendering portfolio widget → ${changeText} (${balance})`)
const content =
-
- return {
+ const variants = {
systemSmall: content,
systemMedium: content,
systemLarge: content,
}
+
+ return renderWidgetToString(variants)
},
renderAndroid: async (req: any) => {
@@ -75,11 +78,12 @@ const handler = createWidgetUpdateNodeHandler({
console.log(`[${now}] [Android] Rendering material colors widget`)
const content =
-
- return [
+ const variants = [
{ size: { width: 200, height: 200 }, content },
{ size: { width: 300, height: 200 }, content },
]
+
+ return renderAndroidWidgetToString(variants)
}
if (req.widgetId !== 'portfolio') {
@@ -93,11 +97,12 @@ const handler = createWidgetUpdateNodeHandler({
console.log(`[${now}] [Android] Rendering portfolio widget → ${changeText} (${balance})`)
const content =
-
- return [
+ const variants = [
{ size: { width: 200, height: 200 }, content },
{ size: { width: 300, height: 200 }, content },
]
+
+ return renderAndroidWidgetToString(variants)
},
validateToken: (token: string) => {
const validToken = token === 'demo-token'
diff --git a/example/widgets/AndroidChartWidget.tsx b/example/widgets/AndroidChartWidget.tsx
index 715d0f2e..128acede 100644
--- a/example/widgets/AndroidChartWidget.tsx
+++ b/example/widgets/AndroidChartWidget.tsx
@@ -1,4 +1,4 @@
-import { VoltraAndroid } from 'voltra/android'
+import { VoltraAndroid } from '@use-voltra/android'
const { BarMark, LineMark, AreaMark, PointMark, RuleMark, SectorMark } = VoltraAndroid
diff --git a/example/widgets/android/AndroidImageFallbackWidget.tsx b/example/widgets/android/AndroidImageFallbackWidget.tsx
index d335bfad..fb329d13 100644
--- a/example/widgets/android/AndroidImageFallbackWidget.tsx
+++ b/example/widgets/android/AndroidImageFallbackWidget.tsx
@@ -1,4 +1,4 @@
-import { VoltraAndroid } from 'voltra/android'
+import { VoltraAndroid } from '@use-voltra/android'
interface ImageFallbackWidgetProps {
example?: 'colors' | 'styled' | 'transparent' | 'custom' | 'mixed'
diff --git a/example/widgets/android/AndroidMaterialColorsWidget.tsx b/example/widgets/android/AndroidMaterialColorsWidget.tsx
index 172c6e71..6310cffb 100644
--- a/example/widgets/android/AndroidMaterialColorsWidget.tsx
+++ b/example/widgets/android/AndroidMaterialColorsWidget.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android'
+import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android'
export type AndroidMaterialColorsRenderSource = 'client' | 'server' | 'initial'
diff --git a/example/widgets/android/AndroidPortfolioWidget.tsx b/example/widgets/android/AndroidPortfolioWidget.tsx
index c8301223..ddb842ef 100644
--- a/example/widgets/android/AndroidPortfolioWidget.tsx
+++ b/example/widgets/android/AndroidPortfolioWidget.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { VoltraAndroid } from 'voltra/android'
+import { VoltraAndroid } from '@use-voltra/android'
const { LineMark, AreaMark } = VoltraAndroid
diff --git a/example/widgets/android/AndroidVoltraWidget.tsx b/example/widgets/android/AndroidVoltraWidget.tsx
index 2d319076..9bc99c9b 100644
--- a/example/widgets/android/AndroidVoltraWidget.tsx
+++ b/example/widgets/android/AndroidVoltraWidget.tsx
@@ -1,4 +1,4 @@
-import { VoltraAndroid } from 'voltra/android'
+import { VoltraAndroid } from '@use-voltra/android'
export const AndroidVoltraWidget = ({ time }: { time: string }) => {
return (
diff --git a/example/widgets/android/updateAndroidVoltraWidget.tsx b/example/widgets/android/updateAndroidVoltraWidget.tsx
index 0c877aeb..0c437de2 100644
--- a/example/widgets/android/updateAndroidVoltraWidget.tsx
+++ b/example/widgets/android/updateAndroidVoltraWidget.tsx
@@ -1,5 +1,5 @@
import { Platform } from 'react-native'
-import { updateAndroidWidget } from 'voltra/android/client'
+import { updateAndroidWidget } from '@use-voltra/android-client'
import { AndroidVoltraWidget } from './AndroidVoltraWidget'
diff --git a/example/widgets/ios/IosPortfolioWidget.tsx b/example/widgets/ios/IosPortfolioWidget.tsx
index 8b72c322..2913e10e 100644
--- a/example/widgets/ios/IosPortfolioWidget.tsx
+++ b/example/widgets/ios/IosPortfolioWidget.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
import type { PortfolioData } from '../android/AndroidPortfolioWidget'
diff --git a/example/widgets/ios/IosWeatherWidget.tsx b/example/widgets/ios/IosWeatherWidget.tsx
index 68f2e1f6..e07a294f 100644
--- a/example/widgets/ios/IosWeatherWidget.tsx
+++ b/example/widgets/ios/IosWeatherWidget.tsx
@@ -1,4 +1,4 @@
-import { Voltra } from 'voltra'
+import { Voltra } from '@use-voltra/ios'
import {
DEFAULT_WEATHER,
diff --git a/example/widgets/ios/ios-portfolio-initial.tsx b/example/widgets/ios/ios-portfolio-initial.tsx
index 2ce7ddb3..ec992488 100644
--- a/example/widgets/ios/ios-portfolio-initial.tsx
+++ b/example/widgets/ios/ios-portfolio-initial.tsx
@@ -1,4 +1,4 @@
-import type { WidgetVariants } from 'voltra'
+import type { WidgetVariants } from '@use-voltra/ios'
import { IosPortfolioWidget } from './IosPortfolioWidget'
diff --git a/example/widgets/ios/ios-weather-initial.tsx b/example/widgets/ios/ios-weather-initial.tsx
index 406746ac..2ea732b6 100644
--- a/example/widgets/ios/ios-weather-initial.tsx
+++ b/example/widgets/ios/ios-weather-initial.tsx
@@ -1,4 +1,4 @@
-import type { WidgetVariants } from 'voltra'
+import type { WidgetVariants } from '@use-voltra/ios'
import { IosWeatherWidget } from './IosWeatherWidget'
diff --git a/package-lock.json b/package-lock.json
index 0b03ffcc..d61ae071 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,9 @@
"example"
],
"devDependencies": {
+ "@babel/plugin-transform-export-namespace-from": "^7.25.9",
"@changesets/cli": "^2.29.8",
+ "@react-native/babel-preset": "0.83.2",
"@types/node": "^20.19.25",
"@types/react": "~19.2.10",
"@types/react-is": "^19.2.0",
@@ -19,6 +21,7 @@
"oxlint": "^1.16.0",
"prettier": "2.8.8",
"react": "19.2.4",
+ "react-native-builder-bob": "^0.41.0",
"ts-node": "^10.9.0",
"turbo": "^2.8.3"
}
@@ -36,6 +39,13 @@
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
+ "@use-voltra/android": "*",
+ "@use-voltra/android-client": "*",
+ "@use-voltra/android-server": "*",
+ "@use-voltra/ios": "*",
+ "@use-voltra/ios-client": "*",
+ "@use-voltra/ios-server": "*",
+ "@use-voltra/server": "*",
"expo": "^55.0.7",
"expo-blur": "~55.0.10",
"expo-build-properties": "~55.0.10",
@@ -62,8 +72,7 @@
"react-native-screens": "~4.23.0",
"react-native-web": "^0.21.0",
"react-native-webview": "13.16.0",
- "react-native-worklets": "~0.7.0",
- "voltra": "*"
+ "react-native-worklets": "~0.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -1520,6 +1529,23 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@ark/schema": {
+ "version": "0.56.0",
+ "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz",
+ "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ark/util": "0.56.0"
+ }
+ },
+ "node_modules/@ark/util": {
+ "version": "0.56.0",
+ "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz",
+ "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/cli": {
"version": "7.27.2",
"dev": true,
@@ -1611,7 +1637,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.27.3",
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1826,7 +1854,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.27.1",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2043,6 +2073,23 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
+ "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1",
"dev": true,
@@ -2580,6 +2627,40 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
+ "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management/node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
+ "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.27.1",
"dev": true,
@@ -3123,6 +3204,22 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-strict-mode": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-strict-mode/-/plugin-transform-strict-mode-7.27.1.tgz",
+ "integrity": "sha512-cdA1TyX9NfOaV8PILyNSrzJxXnjk4UeAgSwSLDCepfOg9AlxCg5al0KWsFh0ZJRzp6k5gwpSlJ4auWT+gx46ig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-transform-template-literals": {
"version": "7.27.1",
"license": "MIT",
@@ -5466,6 +5563,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"license": "MIT",
@@ -6783,18 +6891,24 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.79.6",
+ "version": "0.83.2",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.2.tgz",
+ "integrity": "sha512-XbcN/BEa64pVlb0Hb/E/Ph2SepjVN/FcNKrJcQvtaKZA6mBSO8pW8Eircdlr61/KBH94LihHbQoQDzkQFpeaTg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.3",
- "@react-native/codegen": "0.79.6"
+ "@react-native/codegen": "0.83.2"
},
"engines": {
- "node": ">=18"
+ "node": ">= 20.19.4"
}
},
"node_modules/@react-native/babel-preset": {
- "version": "0.79.6",
+ "version": "0.83.2",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.2.tgz",
+ "integrity": "sha512-X/RAXDfe6W+om/Fw1i6htTxQXFhBJ2jgNOWx3WpI3KbjeIWbq7ib6vrpTeIAW2NUMg+K3mML1NzgD4dpZeqdjA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -6838,39 +6952,70 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
- "@react-native/babel-plugin-codegen": "0.79.6",
- "babel-plugin-syntax-hermes-parser": "0.25.1",
+ "@react-native/babel-plugin-codegen": "0.83.2",
+ "babel-plugin-syntax-hermes-parser": "0.32.0",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
"engines": {
- "node": ">=18"
+ "node": ">= 20.19.4"
},
"peerDependencies": {
"@babel/core": "*"
}
},
+ "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz",
+ "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-parser": "0.32.0"
+ }
+ },
+ "node_modules/@react-native/babel-preset/node_modules/hermes-estree": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz",
+ "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@react-native/babel-preset/node_modules/hermes-parser": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz",
+ "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.32.0"
+ }
+ },
"node_modules/@react-native/codegen": {
- "version": "0.79.6",
+ "version": "0.83.2",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.2.tgz",
+ "integrity": "sha512-9uK6X1miCXqtL4c759l74N/XbQeneWeQVjoV7SD2CGJuW7ZefxaoYenwGPs7rMoCdtS6wuIyR3hXQ+uWEBGYXA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/parser": "^7.25.3",
"glob": "^7.1.1",
- "hermes-parser": "0.25.1",
+ "hermes-parser": "0.32.0",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"yargs": "^17.6.2"
},
"engines": {
- "node": ">=18"
+ "node": ">= 20.19.4"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/@react-native/codegen/node_modules/brace-expansion": {
- "version": "1.1.12",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -6879,6 +7024,9 @@
},
"node_modules/@react-native/codegen/node_modules/glob": {
"version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -6895,8 +7043,25 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@react-native/codegen/node_modules/hermes-estree": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz",
+ "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==",
+ "license": "MIT"
+ },
+ "node_modules/@react-native/codegen/node_modules/hermes-parser": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz",
+ "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.32.0"
+ }
+ },
"node_modules/@react-native/codegen/node_modules/minimatch": {
"version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -7517,6 +7682,19 @@
"version": "0.27.8",
"license": "MIT"
},
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
+ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"license": "BSD-3-Clause",
@@ -8017,6 +8195,10 @@
"resolved": "packages/expo-plugin",
"link": true
},
+ "node_modules/@use-voltra/generator": {
+ "resolved": "packages/generator",
+ "link": true
+ },
"node_modules/@use-voltra/ios": {
"resolved": "packages/ios",
"link": true
@@ -8477,6 +8659,28 @@
"node": ">=10"
}
},
+ "node_modules/arkregex": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz",
+ "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ark/util": "0.56.0"
+ }
+ },
+ "node_modules/arktype": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.0.tgz",
+ "integrity": "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ark/schema": "0.56.0",
+ "@ark/util": "0.56.0",
+ "arkregex": "0.0.5"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"dev": true,
@@ -8864,6 +9068,142 @@
}
}
},
+ "node_modules/babel-preset-expo/node_modules/@react-native/babel-plugin-codegen": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.6.tgz",
+ "integrity": "sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.3",
+ "@react-native/codegen": "0.79.6"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/babel-preset-expo/node_modules/@react-native/babel-preset": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.6.tgz",
+ "integrity": "sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/plugin-proposal-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-transform-arrow-functions": "^7.24.7",
+ "@babel/plugin-transform-async-generator-functions": "^7.25.4",
+ "@babel/plugin-transform-async-to-generator": "^7.24.7",
+ "@babel/plugin-transform-block-scoping": "^7.25.0",
+ "@babel/plugin-transform-class-properties": "^7.25.4",
+ "@babel/plugin-transform-classes": "^7.25.4",
+ "@babel/plugin-transform-computed-properties": "^7.24.7",
+ "@babel/plugin-transform-destructuring": "^7.24.8",
+ "@babel/plugin-transform-flow-strip-types": "^7.25.2",
+ "@babel/plugin-transform-for-of": "^7.24.7",
+ "@babel/plugin-transform-function-name": "^7.25.1",
+ "@babel/plugin-transform-literals": "^7.25.2",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
+ "@babel/plugin-transform-numeric-separator": "^7.24.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.24.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
+ "@babel/plugin-transform-parameters": "^7.24.7",
+ "@babel/plugin-transform-private-methods": "^7.24.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
+ "@babel/plugin-transform-react-display-name": "^7.24.7",
+ "@babel/plugin-transform-react-jsx": "^7.25.2",
+ "@babel/plugin-transform-react-jsx-self": "^7.24.7",
+ "@babel/plugin-transform-react-jsx-source": "^7.24.7",
+ "@babel/plugin-transform-regenerator": "^7.24.7",
+ "@babel/plugin-transform-runtime": "^7.24.7",
+ "@babel/plugin-transform-shorthand-properties": "^7.24.7",
+ "@babel/plugin-transform-spread": "^7.24.7",
+ "@babel/plugin-transform-sticky-regex": "^7.24.7",
+ "@babel/plugin-transform-typescript": "^7.25.2",
+ "@babel/plugin-transform-unicode-regex": "^7.24.7",
+ "@babel/template": "^7.25.0",
+ "@react-native/babel-plugin-codegen": "0.79.6",
+ "babel-plugin-syntax-hermes-parser": "0.25.1",
+ "babel-plugin-transform-flow-enums": "^0.0.2",
+ "react-refresh": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/babel-preset-expo/node_modules/@react-native/codegen": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.6.tgz",
+ "integrity": "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/parser": "^7.25.3",
+ "glob": "^7.1.1",
+ "hermes-parser": "0.25.1",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "yargs": "^17.6.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/babel-preset-expo/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/babel-preset-expo/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/babel-preset-expo/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/babel-preset-jest": {
"version": "29.6.3",
"license": "MIT",
@@ -8905,10 +9245,15 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.19",
+ "version": "2.10.29",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
+ "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/better-opn": {
@@ -9183,7 +9528,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001769",
+ "version": "1.0.30001792",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
+ "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
"funding": [
{
"type": "opencollective",
@@ -9869,6 +10216,98 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/del": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/del/-/del-8.0.1.tgz",
+ "integrity": "sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "globby": "^14.0.2",
+ "is-glob": "^4.0.3",
+ "is-path-cwd": "^3.0.0",
+ "is-path-inside": "^4.0.0",
+ "p-map": "^7.0.2",
+ "presentable-error": "^0.0.1",
+ "slash": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/del/node_modules/globby": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
+ "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^2.1.0",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.3",
+ "path-type": "^6.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/del/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/del/node_modules/p-map": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
+ "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/del/node_modules/path-type": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/del/node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"dev": true,
@@ -10023,7 +10462,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.286",
+ "version": "1.5.355",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.355.tgz",
+ "integrity": "sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==",
"license": "ISC"
},
"node_modules/emittery": {
@@ -11660,6 +12101,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"dev": true,
@@ -12227,6 +12681,20 @@
"loose-envify": "^1.0.0"
}
},
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"dev": true,
@@ -12438,6 +12906,145 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-git-dirty": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-git-dirty/-/is-git-dirty-2.0.2.tgz",
+ "integrity": "sha512-U3YCo+GKR/rDsY7r0v/LBICbQwsx859tDQnAT+v0E/zCDeWbQ1TUt1FtyExeyik7VIJlYOLHCIifLdz71HDalg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^4.0.3",
+ "is-git-repository": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/is-git-dirty/node_modules/execa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+ "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/is-git-dirty/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-git-dirty/node_modules/human-signals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.12.0"
+ }
+ },
+ "node_modules/is-git-dirty/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-git-repository": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-git-repository/-/is-git-repository-2.0.0.tgz",
+ "integrity": "sha512-HDO50CG5suIAcmqG4F1buqVXEZRPn+RaXIn9pFKq/947FBo2bCRwK7ZluEVZOy99a4IQyqsjbKEpAiOXCccOHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^4.0.3",
+ "is-absolute": "^1.0.0"
+ }
+ },
+ "node_modules/is-git-repository/node_modules/execa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+ "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/is-git-repository/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-git-repository/node_modules/human-signals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.12.0"
+ }
+ },
+ "node_modules/is-git-repository/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"dev": true,
@@ -12493,6 +13100,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-path-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz",
+ "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
+ "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"dev": true,
@@ -12515,6 +13148,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-set": {
"version": "2.0.3",
"dev": true,
@@ -12606,6 +13252,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"license": "MIT",
@@ -14894,7 +15553,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.27",
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
"license": "MIT"
},
"node_modules/normalize-path": {
@@ -15695,6 +16356,19 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/presentable-error": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/presentable-error/-/presentable-error-0.0.1.tgz",
+ "integrity": "sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/prettier": {
"version": "2.8.8",
"dev": true,
@@ -16061,39 +16735,1535 @@
}
}
},
- "node_modules/react-native-edge-to-edge": {
- "version": "1.6.0",
- "license": "MIT",
- "peer": true,
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-harness": {
- "version": "1.0.0-alpha.23",
+ "node_modules/react-native-builder-bob": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/react-native-builder-bob/-/react-native-builder-bob-0.41.0.tgz",
+ "integrity": "sha512-avuPbEUz4n4w2aCgTsOAA9JOYLXMYZSKWKf84EO0CADsbf2UWl/yXVyGgKmM9ueGThBRvKJtfMh8DLSInGZuSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@react-native-harness/babel-preset": "1.0.0-alpha.23",
- "@react-native-harness/cli": "1.0.0-alpha.23",
- "@react-native-harness/jest": "1.0.0-alpha.23",
- "@react-native-harness/metro": "1.0.0-alpha.23",
- "@react-native-harness/runtime": "1.0.0-alpha.23",
- "tslib": "^2.3.0"
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-flow-strip-types": "^7.27.1",
+ "@babel/plugin-transform-strict-mode": "^7.27.1",
+ "@babel/preset-env": "^7.29.2",
+ "@babel/preset-react": "^7.28.5",
+ "@babel/preset-typescript": "^7.28.5",
+ "arktype": "^2.2.0",
+ "babel-plugin-syntax-hermes-parser": "^0.34.0",
+ "browserslist": "^4.28.2",
+ "cross-spawn": "^7.0.6",
+ "dedent": "^1.7.2",
+ "del": "^8.0.1",
+ "escape-string-regexp": "^5.0.0",
+ "fs-extra": "^11.3.4",
+ "glob": "^13.0.6",
+ "is-git-dirty": "^2.0.2",
+ "json5": "^2.2.3",
+ "kleur": "^4.1.5",
+ "prompts": "^2.4.2",
+ "react-native-monorepo-config": "^0.3.3",
+ "which": "^6.0.1",
+ "yargs": "^18.0.0"
},
"bin": {
- "react-native-harness": "bin.js"
+ "bob": "bin/bob"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >= 23.4.0"
}
},
- "node_modules/react-native-is-edge-to-edge": {
- "version": "1.3.1",
+ "node_modules/react-native-builder-bob/node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz",
+ "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.29.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "regexpu-core": "^6.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
+ "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "debug": "^4.4.3",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.11"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helper-replace-supers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+ "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
+ "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
+ "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
+ "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+ "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+ "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz",
+ "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-remap-async-to-generator": "^7.27.1",
+ "@babel/traverse": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz",
+ "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-remap-async-to-generator": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz",
+ "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz",
+ "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
+ "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-classes": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz",
+ "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-replace-supers": "^7.28.6",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz",
+ "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/template": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
+ "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
+ "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz",
+ "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
+ "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
+ "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz",
+ "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz",
+ "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.29.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
+ "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz",
+ "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz",
+ "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz",
+ "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz",
+ "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz",
+ "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz",
+ "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
+ "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz",
+ "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz",
+ "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-react-display-name": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz",
+ "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz",
+ "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
+ "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-spread": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz",
+ "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+ "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
+ "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
+ "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/preset-env": {
+ "version": "7.29.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
+ "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.3",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.28.6",
+ "@babel/plugin-syntax-import-attributes": "^7.28.6",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.0",
+ "@babel/plugin-transform-async-to-generator": "^7.28.6",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.28.6",
+ "@babel/plugin-transform-class-properties": "^7.28.6",
+ "@babel/plugin-transform-class-static-block": "^7.28.6",
+ "@babel/plugin-transform-classes": "^7.28.6",
+ "@babel/plugin-transform-computed-properties": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-dotall-regex": "^7.28.6",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-explicit-resource-management": "^7.28.6",
+ "@babel/plugin-transform-exponentiation-operator": "^7.28.6",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.28.6",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.28.6",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.28.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.4",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
+ "@babel/plugin-transform-numeric-separator": "^7.28.6",
+ "@babel/plugin-transform-object-rest-spread": "^7.28.6",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.28.6",
+ "@babel/plugin-transform-optional-chaining": "^7.28.6",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/plugin-transform-private-methods": "^7.28.6",
+ "@babel/plugin-transform-private-property-in-object": "^7.28.6",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.29.0",
+ "@babel/plugin-transform-regexp-modifiers": "^7.28.6",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.28.6",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.28.6",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
+ "core-js-compat": "^3.48.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/preset-react": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz",
+ "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-transform-react-display-name": "^7.28.0",
+ "@babel/plugin-transform-react-jsx": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-development": "^7.27.1",
+ "@babel/plugin-transform-react-pure-annotations": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/@babel/preset-typescript": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
+ "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
+ "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
+ "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "core-js-compat": "^3.48.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/babel-plugin-syntax-hermes-parser": {
+ "version": "0.34.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.34.0.tgz",
+ "integrity": "sha512-q4xeAymMrot/21MHA3+fd5mcFF7stx6ntKFO/Of5ldyDpgTBcK1l0NiHAh4NdHHdb4aHqHgQOy7r6yk0IIlz8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-parser": "0.34.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/core-js-compat": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
+ "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/dedent": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
+ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-native-builder-bob/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/fs-extra": {
+ "version": "11.3.5",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
+ "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/hermes-estree": {
+ "version": "0.34.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.34.0.tgz",
+ "integrity": "sha512-6qLylexjmuKa/YYhMiNn/3VejBsdzwmYUGmNpc693/pJzymmbufhkRW/2K6GqFgu0ApRWoqF0NbM6u82jFcOXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-native-builder-bob/node_modules/hermes-parser": {
+ "version": "0.34.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.34.0.tgz",
+ "integrity": "sha512-tcgan5UNZvu3WwmR3jDAlmwEAR2CMv8cwQVMe5j0NrLQkstf0l3ULbYPuTZWbXxbPa0PyZPiq5LYEcFVmhM9LQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.34.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/regexpu-core": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.2",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.13.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/regjsparser": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz",
+ "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.1.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/react-native-builder-bob/node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/react-native-edge-to-edge": {
+ "version": "1.6.0",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-harness": {
+ "version": "1.0.0-alpha.23",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-harness/babel-preset": "1.0.0-alpha.23",
+ "@react-native-harness/cli": "1.0.0-alpha.23",
+ "@react-native-harness/jest": "1.0.0-alpha.23",
+ "@react-native-harness/metro": "1.0.0-alpha.23",
+ "@react-native-harness/runtime": "1.0.0-alpha.23",
+ "tslib": "^2.3.0"
+ },
+ "bin": {
+ "react-native-harness": "bin.js"
+ }
+ },
+ "node_modules/react-native-is-edge-to-edge": {
+ "version": "1.3.1",
+ "license": "MIT",
+ "peerDependencies": {
"react": "*",
"react-native": "*"
}
},
+ "node_modules/react-native-monorepo-config": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/react-native-monorepo-config/-/react-native-monorepo-config-0.3.3.tgz",
+ "integrity": "sha512-d1kjHRVsbd/yVkFb5rYxWYiWVzCqJgUzcc499ejfc8jH6q6XYB4U+dNpX/HsxdXZLpzEhmKvgq0PsjRAIdAVeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0",
+ "fast-glob": "^3.3.3"
+ }
+ },
+ "node_modules/react-native-monorepo-config/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.4.0",
"license": "MIT",
@@ -16139,25 +18309,6 @@
"react-native": "*"
}
},
- "node_modules/react-native/node_modules/@react-native/codegen": {
- "version": "0.83.2",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/parser": "^7.25.3",
- "glob": "^7.1.1",
- "hermes-parser": "0.32.0",
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1",
- "yargs": "^17.6.2"
- },
- "engines": {
- "node": ">= 20.19.4"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
"version": "0.83.2",
"license": "MIT"
@@ -18484,6 +20635,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/undici": {
"version": "6.24.1",
"license": "MIT",
@@ -18515,7 +20676,9 @@
}
},
"node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.2.0",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
"license": "MIT",
"engines": {
"node": ">=4"
@@ -18528,6 +20691,19 @@
"node": ">=4"
}
},
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/unimodules-app-loader": {
"version": "55.0.4",
"license": "MIT"
@@ -18783,10 +20959,6 @@
"version": "1.0.1",
"license": "MIT"
},
- "node_modules/voltra": {
- "resolved": "packages/voltra",
- "link": true
- },
"node_modules/voltra-example": {
"resolved": "example",
"link": true
@@ -19403,7 +21575,17 @@
"version": "1.4.1",
"license": "MIT",
"dependencies": {
- "@use-voltra/android": "1.4.1"
+ "@babel/core": "^7.27.4",
+ "@expo/config-plugins": "~10.1.2",
+ "@use-voltra/android": "1.4.1",
+ "@use-voltra/expo-plugin": "1.4.1",
+ "vd-tool": "^4.0.2"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.14",
+ "@types/node": "^20.19.25",
+ "jest": "^29.7.0",
+ "ts-jest": "^29.3.4"
},
"peerDependencies": {
"expo": "*",
@@ -19411,6 +21593,85 @@
"react-native": "*"
}
},
+ "packages/android-client/node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "packages/android-client/node_modules/ts-jest": {
+ "version": "29.4.9",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
+ "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "^0.2.6",
+ "fast-json-stable-stringify": "^2.1.0",
+ "handlebars": "^4.7.9",
+ "json5": "^2.2.3",
+ "lodash.memoize": "^4.1.2",
+ "make-error": "^1.3.6",
+ "semver": "^7.7.4",
+ "type-fest": "^4.41.0",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/transform": "^29.0.0 || ^30.0.0",
+ "@jest/types": "^29.0.0 || ^30.0.0",
+ "babel-jest": "^29.0.0 || ^30.0.0",
+ "jest": "^29.0.0 || ^30.0.0",
+ "jest-util": "^29.0.0 || ^30.0.0",
+ "typescript": ">=4.3 <7"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/transform": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jest-util": {
+ "optional": true
+ }
+ }
+ },
+ "packages/android-client/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"packages/android-server": {
"name": "@use-voltra/android-server",
"version": "1.4.1",
@@ -19440,12 +21701,7 @@
"version": "1.4.1",
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.27.4",
- "@expo/config-plugins": "~10.1.2",
- "@expo/plist": "^0.3.5",
- "dedent": "^1.7.1",
- "vd-tool": "^4.0.2",
- "xcode": "^3.0.1"
+ "@babel/core": "^7.27.4"
},
"devDependencies": {
"@types/jest": "^29.5.14",
@@ -19532,6 +21788,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/generator": {
+ "name": "@use-voltra/generator",
+ "version": "1.4.1",
+ "devDependencies": {
+ "prettier": "2.8.8",
+ "ts-node": "^10.9.0"
+ }
+ },
"packages/ios": {
"name": "@use-voltra/ios",
"version": "1.4.1",
@@ -19539,6 +21803,10 @@
"dependencies": {
"@use-voltra/core": "1.4.1"
},
+ "devDependencies": {
+ "react-native-builder-bob": "^0.41.0",
+ "typescript": "~5.9.2"
+ },
"peerDependencies": {
"react": "*"
}
@@ -19548,7 +21816,19 @@
"version": "1.4.1",
"license": "MIT",
"dependencies": {
- "@use-voltra/ios": "1.4.1"
+ "@babel/core": "^7.27.4",
+ "@expo/config-plugins": "~10.1.2",
+ "@expo/plist": "^0.3.5",
+ "@use-voltra/expo-plugin": "1.4.1",
+ "@use-voltra/ios": "1.4.1",
+ "dedent": "^1.7.1",
+ "xcode": "^3.0.1"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.14",
+ "@types/node": "^20.19.25",
+ "jest": "^29.7.0",
+ "ts-jest": "^29.3.4"
},
"peerDependencies": {
"expo": "*",
@@ -19556,6 +21836,85 @@
"react-native": "*"
}
},
+ "packages/ios-client/node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "packages/ios-client/node_modules/ts-jest": {
+ "version": "29.4.9",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz",
+ "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "^0.2.6",
+ "fast-json-stable-stringify": "^2.1.0",
+ "handlebars": "^4.7.9",
+ "json5": "^2.2.3",
+ "lodash.memoize": "^4.1.2",
+ "make-error": "^1.3.6",
+ "semver": "^7.7.4",
+ "type-fest": "^4.41.0",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/transform": "^29.0.0 || ^30.0.0",
+ "@jest/types": "^29.0.0 || ^30.0.0",
+ "babel-jest": "^29.0.0 || ^30.0.0",
+ "jest": "^29.0.0 || ^30.0.0",
+ "jest-util": "^29.0.0 || ^30.0.0",
+ "typescript": ">=4.3 <7"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/transform": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jest-util": {
+ "optional": true
+ }
+ }
+ },
+ "packages/ios-client/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"packages/ios-server": {
"name": "@use-voltra/ios-server",
"version": "1.4.1",
@@ -19569,31 +21928,24 @@
"react": "*"
}
},
+ "packages/ios/node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"packages/server": {
"name": "@use-voltra/server",
"version": "1.4.1",
"license": "MIT"
- },
- "packages/voltra": {
- "version": "1.4.1",
- "license": "MIT",
- "dependencies": {
- "@use-voltra/android": "1.4.1",
- "@use-voltra/android-client": "1.4.1",
- "@use-voltra/android-server": "1.4.1",
- "@use-voltra/core": "1.4.1",
- "@use-voltra/expo-plugin": "1.4.1",
- "@use-voltra/ios": "1.4.1",
- "@use-voltra/ios-client": "1.4.1",
- "@use-voltra/ios-server": "1.4.1",
- "@use-voltra/server": "1.4.1",
- "react-is": "^19.2.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
}
}
}
diff --git a/package.json b/package.json
index 84cef0e1..176fa317 100644
--- a/package.json
+++ b/package.json
@@ -7,30 +7,30 @@
"example"
],
"scripts": {
- "build": "npm run build --workspace voltra",
+ "build": "turbo run build",
"build:all": "turbo run build",
- "build:plugin": "npm run build --workspace @use-voltra/expo-plugin",
+ "build:plugin": "npm run build --workspace @use-voltra/expo-plugin && npm run build:expo-plugin --workspace @use-voltra/ios-client && npm run build:expo-plugin --workspace @use-voltra/android-client",
"clean": "turbo run clean",
- "clean:plugin": "npm run clean --workspace @use-voltra/expo-plugin",
- "format:check": "npm run format:check --workspace voltra",
- "format:fix": "npm run format:fix --workspace voltra",
+ "clean:plugin": "npm run clean --workspace @use-voltra/expo-plugin && npm run clean:expo-plugin --workspace @use-voltra/ios-client && npm run clean:expo-plugin --workspace @use-voltra/android-client",
+ "format:check": "npm run format:js:check && npm run format:check --workspace @use-voltra/android-client && npm run format:check --workspace @use-voltra/ios-client",
+ "format:fix": "npm run format:js:fix && npm run format:fix --workspace @use-voltra/android-client && npm run format:fix --workspace @use-voltra/ios-client",
"format:js:check": "prettier --check .",
"format:js:fix": "prettier --write .",
- "format:kotlin:check": "npm run format:kotlin:check --workspace voltra",
- "format:kotlin:fix": "npm run format:kotlin:fix --workspace voltra",
- "format:swift:check": "npm run format:swift:check --workspace voltra",
- "format:swift:fix": "npm run format:swift:fix --workspace voltra",
+ "format:kotlin:check": "npm run format:kotlin:check --workspace @use-voltra/android-client",
+ "format:kotlin:fix": "npm run format:kotlin:fix --workspace @use-voltra/android-client",
+ "format:swift:check": "npm run format:swift:check --workspace @use-voltra/ios-client",
+ "format:swift:fix": "npm run format:swift:fix --workspace @use-voltra/ios-client",
"lint": "turbo run lint",
"lint:libOnly": "npm run lint",
- "lint:fix": "npm run lint:fix --workspace voltra",
- "test": "npm run test --workspace voltra",
- "test:js": "npm run test:js --workspace voltra",
- "test:kotlin": "npm run test:kotlin --workspace voltra",
- "test:swift": "npm run test:swift --workspace voltra",
- "test:native": "npm run test:native --workspace voltra",
- "test:all": "npm run test:all --workspace voltra",
+ "lint:fix": "oxlint --fix .",
+ "test": "turbo run test",
+ "test:js": "npm run test",
+ "test:kotlin": "npm run test:kotlin --workspace @use-voltra/android-client",
+ "test:swift": "npm run test:swift --workspace @use-voltra/ios-client",
+ "test:native": "npm run test:kotlin --workspace @use-voltra/android-client && npm run test:swift --workspace @use-voltra/ios-client",
+ "test:all": "npm run test:js && npm run test:native",
"typecheck": "turbo run typecheck",
- "generate": "ts-node generator/generate-types.ts",
+ "generate": "npm run generate --workspace @use-voltra/generator",
"harness:ios": "npm run harness:ios --workspace voltra-example",
"harness:android": "npm run harness:android --workspace voltra-example",
"changeset": "changeset",
@@ -38,7 +38,9 @@
"release": "changeset publish"
},
"devDependencies": {
+ "@babel/plugin-transform-export-namespace-from": "^7.25.9",
"@changesets/cli": "^2.29.8",
+ "@react-native/babel-preset": "0.83.2",
"@types/node": "^20.19.25",
"@types/react": "~19.2.10",
"@types/react-is": "^19.2.0",
@@ -48,7 +50,8 @@
"prettier": "2.8.8",
"react": "19.2.4",
"ts-node": "^10.9.0",
- "turbo": "^2.8.3"
+ "turbo": "^2.8.3",
+ "react-native-builder-bob": "^0.41.0"
},
"overrides": {
"react": "19.2.4",
diff --git a/packages/android-client/README.md b/packages/android-client/README.md
new file mode 100644
index 00000000..f275ff1a
--- /dev/null
+++ b/packages/android-client/README.md
@@ -0,0 +1,121 @@
+
+
+### Voltra for Android — React Native client
+
+[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
+
+`@use-voltra/android-client` is the Android React Native package for Voltra. It re-exports the `VoltraAndroid` JSX namespace and related APIs from `@use-voltra/android` (installed automatically as a dependency) and provides runtime APIs for Home Screen widgets, ongoing notifications, development previews, event listeners, and the Expo config plugin.
+
+## Features
+
+- **Home Screen widgets**: Update, reload, pin, and query widgets with `updateAndroidWidget`, `reloadAndroidWidgets`, `getActiveWidgets`, and more.
+
+- **Ongoing notifications**: Start and update promoted ongoing notifications with `useAndroidOngoingNotification` and related APIs.
+
+- **Fast Refresh**: Previews integrate with your React Native dev workflow via `VoltraWidgetPreview` and `VoltraView`.
+
+- **Image preloading**: Download remote images for widgets with `preloadImages` and `reloadWidgets`.
+
+- **Server-driven widgets**: Store credentials for background widget fetches with `setWidgetServerCredentials`.
+
+- **Expo config plugin**: Add `"@use-voltra/android-client"` to `app.json` to register widgets, optional notifications, and build-time initial states.
+
+## Documentation
+
+The documentation is available at [use-voltra.dev](https://use-voltra.dev). Relevant topics for this package:
+
+- [Installation](https://use-voltra.dev/getting-started/installation)
+- [Android Setup](https://use-voltra.dev/android/setup)
+- [Developing Widgets](https://use-voltra.dev/android/development/developing-widgets)
+- [Managing Ongoing Notifications](https://use-voltra.dev/android/development/managing-ongoing-notifications)
+- [Plugin Configuration](https://use-voltra.dev/android/api/plugin-configuration)
+
+## Getting started
+
+> [!NOTE]
+> Voltra isn't supported in Expo Go. Use [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/) or a native build.
+
+Install the Android client package:
+
+```sh
+npm install @use-voltra/android-client
+```
+
+Add the Expo plugin to your `app.json`:
+
+```json
+{
+ "expo": {
+ "plugins": [
+ [
+ "@use-voltra/android-client",
+ {
+ "enableNotifications": true,
+ "widgets": [
+ {
+ "id": "my_widget",
+ "displayName": "My Widget",
+ "description": "A Voltra widget",
+ "targetCellWidth": 2,
+ "targetCellHeight": 2
+ }
+ ]
+ }
+ ]
+ ]
+ }
+}
+```
+
+Then run `npx expo prebuild --platform android` to generate the native project changes.
+
+See the [Android setup guide](https://use-voltra.dev/android/setup) for detailed instructions.
+
+## Quick example
+
+```tsx
+import { updateAndroidWidget, VoltraAndroid } from '@use-voltra/android-client'
+
+const WeatherWidget = ({ temperature, condition }: { temperature: number; condition: string }) => (
+
+
+ {temperature}°C
+ {condition}
+
+
+)
+
+export async function refreshWeatherWidget() {
+ await updateAndroidWidget('my_widget', )
+}
+```
+
+## Platform compatibility
+
+This package targets **Android** with Jetpack Compose Glance widgets. Import UI and runtime APIs from `@use-voltra/android-client`. For server-side rendering, use `@use-voltra/android-server` in your backend only.
+
+## 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].
+
+If you think it's cool, please star it 🌟. This project will always remain free to use.
+
+[Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
+
+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/@use-voltra/android-client?style=for-the-badge
+[license]: https://github.com/callstackincubator/voltra/blob/main/LICENSE.txt
+[npm-downloads-badge]: https://img.shields.io/npm/dm/@use-voltra/android-client?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/android-client
+[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
+[prs-welcome]: ../../CONTRIBUTING.md
diff --git a/packages/voltra/android/build.gradle b/packages/android-client/android/build.gradle
similarity index 60%
rename from packages/voltra/android/build.gradle
rename to packages/android-client/android/build.gradle
index 3fd6edb0..722bf3bf 100644
--- a/packages/voltra/android/build.gradle
+++ b/packages/android-client/android/build.gradle
@@ -1,54 +1,26 @@
buildscript {
- // 1. ADD THIS BLOCK TO RESOLVE THE PLUGIN
repositories {
google()
mavenCentral()
}
dependencies {
- // Ensure this version matches your project's Kotlin version (e.g., 2.0.0, 2.0.20)
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.0.0"
classpath "org.jetbrains.kotlin:kotlin-serialization:2.0.21"
}
- // Existing helper
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
apply plugin: 'com.android.library'
+apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
+apply plugin: 'com.facebook.react'
-group = 'voltra'
-version = '0.1.0'
-
-def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
-apply from: expoModulesCorePlugin
-applyKotlinExpoModulesCorePlugin()
-useCoreDependencies()
-useExpoPublishing()
-
-// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
-// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
-// Most of the time, you may like to manage the Android SDK versions yourself.
-def useManagedAndroidSdkVersions = false
-if (useManagedAndroidSdkVersions) {
- useDefaultAndroidSdkVersions()
-} else {
- buildscript {
- // Simple helper that allows the root project to override versions declared by this library.
- ext.safeExtGet = { prop, fallback ->
- rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
- }
- }
- project.android {
- compileSdkVersion safeExtGet("compileSdkVersion", 36)
- defaultConfig {
- minSdkVersion safeExtGet("minSdkVersion", 31)
- targetSdkVersion safeExtGet("targetSdkVersion", 36)
- }
- }
+def safeExtGet = { prop, fallback ->
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Read version from package.json for BuildConfig
@@ -58,20 +30,26 @@ def voltraVersion = packageJson.version
android {
namespace "voltra"
+
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
+
defaultConfig {
+ minSdkVersion safeExtGet("minSdkVersion", 31)
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
versionCode 1
versionName "0.1.0"
buildConfigField "String", "VOLTRA_VERSION", "\"${voltraVersion}\""
}
+
buildFeatures {
buildConfig true
+ compose true
}
+
lintOptions {
abortOnError false
}
- buildFeatures {
- compose true
- }
+
testOptions {
unitTests.all {
testLogging {
@@ -86,7 +64,10 @@ android {
}
dependencies {
- // Jetpack Glance - use 'api' instead of 'implementation' to make these available to consuming apps
+ // React Native
+ implementation "com.facebook.react:react-android"
+
+ // Jetpack Glance
api "androidx.glance:glance:1.2.0-rc01"
api "androidx.glance:glance-appwidget:1.2.0-rc01"
diff --git a/packages/voltra/android/src/main/AndroidManifest.xml b/packages/android-client/android/src/main/AndroidManifest.xml
similarity index 100%
rename from packages/voltra/android/src/main/AndroidManifest.xml
rename to packages/android-client/android/src/main/AndroidManifest.xml
diff --git a/packages/android-client/android/src/main/java/voltra/VoltraModule.kt b/packages/android-client/android/src/main/java/voltra/VoltraModule.kt
new file mode 100644
index 00000000..f9e1a6dc
--- /dev/null
+++ b/packages/android-client/android/src/main/java/voltra/VoltraModule.kt
@@ -0,0 +1,364 @@
+package voltra
+
+import android.appwidget.AppWidgetManager
+import android.util.Log
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.appwidget.GlanceAppWidgetManager
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.WritableNativeMap
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.runBlocking
+import voltra.images.VoltraImageManager
+import voltra.widget.VoltraGlanceWidget
+import voltra.widget.VoltraWidgetManager
+
+class VoltraModule(
+ reactContext: ReactApplicationContext,
+) : NativeVoltraAndroidSpec(reactContext) {
+ companion object {
+ private const val TAG = "VoltraModule"
+ }
+
+ private val notificationManager by lazy {
+ VoltraNotificationManager(reactApplicationContext)
+ }
+
+ private val widgetManager by lazy {
+ VoltraWidgetManager(reactApplicationContext)
+ }
+
+ private val imageManager by lazy {
+ VoltraImageManager(reactApplicationContext)
+ }
+
+ override fun startAndroidOngoingNotification(
+ payload: String,
+ options: ReadableMap,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "startAndroidOngoingNotification called")
+ val opts = AndroidOngoingNotificationOptions(options)
+ val result = runBlocking { notificationManager.startOngoingNotification(payload, opts) }
+ Log.d(TAG, "startAndroidOngoingNotification returning: $result")
+ promise.resolve(result.toWritableMap())
+ }
+
+ override fun updateAndroidOngoingNotification(
+ notificationId: String,
+ payload: String,
+ options: ReadableMap?,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "updateAndroidOngoingNotification called with notificationId=$notificationId")
+ val opts =
+ options?.let { AndroidOngoingNotificationOptions(it) }
+ ?: AndroidOngoingNotificationOptions()
+ val result =
+ runBlocking {
+ notificationManager.updateOngoingNotification(notificationId, payload, opts)
+ }
+ Log.d(TAG, "updateAndroidOngoingNotification returning: $result")
+ promise.resolve(result.toWritableMap())
+ }
+
+ override fun upsertAndroidOngoingNotification(
+ payload: String,
+ options: ReadableMap,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "upsertAndroidOngoingNotification called")
+ val opts = AndroidOngoingNotificationOptions(options)
+ val result = runBlocking { notificationManager.upsertOngoingNotification(payload, opts) }
+ Log.d(TAG, "upsertAndroidOngoingNotification returning: $result")
+ promise.resolve(result.toWritableMap())
+ }
+
+ override fun stopAndroidOngoingNotification(
+ notificationId: String,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "stopAndroidOngoingNotification called with notificationId=$notificationId")
+ val result = notificationManager.stopOngoingNotification(notificationId)
+ promise.resolve(result.toWritableMap())
+ }
+
+ override fun isAndroidOngoingNotificationActive(notificationId: String): Boolean =
+ notificationManager.isOngoingNotificationActive(notificationId)
+
+ override fun getAndroidOngoingNotificationStatus(notificationId: String): WritableNativeMap {
+ val status = notificationManager.getOngoingNotificationStatus(notificationId)
+ return WritableNativeMap().apply {
+ putBoolean("isActive", status.isActive)
+ putBoolean("isDismissed", status.isDismissed)
+ putBoolean("isPromoted", status.isPromoted ?: false)
+ putBoolean("hasPromotableCharacteristics", status.hasPromotableCharacteristics ?: false)
+ }
+ }
+
+ override fun endAllAndroidOngoingNotifications(promise: Promise) {
+ runBlocking { notificationManager.endAllOngoingNotifications() }
+ promise.resolve(null)
+ }
+
+ override fun canPostPromotedAndroidNotifications(): Boolean =
+ notificationManager.canPostPromotedAndroidNotifications()
+
+ override fun getAndroidOngoingNotificationCapabilities(): WritableNativeMap {
+ val capabilities = notificationManager.getOngoingNotificationCapabilities()
+ return WritableNativeMap().apply {
+ putInt("apiLevel", capabilities.apiLevel)
+ putBoolean("notificationsEnabled", capabilities.notificationsEnabled)
+ putBoolean("supportsPromotedNotifications", capabilities.supportsPromotedNotifications)
+ putBoolean("canPostPromotedNotifications", capabilities.canPostPromotedNotifications)
+ putBoolean("canRequestPromotedOngoing", capabilities.canRequestPromotedOngoing)
+ }
+ }
+
+ override fun openAndroidNotificationSettings(promise: Promise) {
+ runBlocking { notificationManager.openPromotedNotificationSettings() }
+ promise.resolve(null)
+ }
+
+ override fun updateAndroidWidget(
+ widgetId: String,
+ jsonString: String,
+ options: ReadableMap?,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "updateAndroidWidget called with widgetId=$widgetId")
+ val deepLinkUrl = options?.getString("deepLinkUrl")
+ widgetManager.writeWidgetData(widgetId, jsonString, deepLinkUrl)
+ runBlocking { widgetManager.updateWidget(widgetId) }
+ Log.d(TAG, "updateAndroidWidget completed")
+ promise.resolve(null)
+ }
+
+ override fun reloadAndroidWidgets(
+ widgetIds: ReadableArray?,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "reloadAndroidWidgets called with widgetIds=$widgetIds")
+ val ids: List? =
+ widgetIds?.let { array ->
+ (0 until array.size()).mapNotNull { array.getString(it) }
+ }
+ runBlocking { widgetManager.reloadWidgets(ids) }
+ Log.d(TAG, "reloadAndroidWidgets completed")
+ promise.resolve(null)
+ }
+
+ override fun clearAndroidWidget(
+ widgetId: String,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "clearAndroidWidget called with widgetId=$widgetId")
+ widgetManager.clearWidgetData(widgetId)
+ runBlocking { widgetManager.updateWidget(widgetId) }
+ Log.d(TAG, "clearAndroidWidget completed")
+ promise.resolve(null)
+ }
+
+ override fun clearAllAndroidWidgets(promise: Promise) {
+ Log.d(TAG, "clearAllAndroidWidgets called")
+ widgetManager.clearAllWidgetData()
+ runBlocking { widgetManager.reloadAllWidgets() }
+ Log.d(TAG, "clearAllAndroidWidgets completed")
+ promise.resolve(null)
+ }
+
+ override fun requestPinGlanceAppWidget(
+ widgetId: String,
+ options: ReadableMap?,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "requestPinGlanceAppWidget called with widgetId=$widgetId")
+ val receiverClassName = "${reactApplicationContext.packageName}.widget.VoltraWidget_${widgetId}Receiver"
+ Log.d(TAG, "Looking for receiver: $receiverClassName")
+
+ val receiverClass =
+ try {
+ @Suppress("UNCHECKED_CAST")
+ Class.forName(receiverClassName) as Class
+ } catch (e: ClassNotFoundException) {
+ Log.e(TAG, "Widget receiver class not found: $receiverClassName", e)
+ promise.reject("requestPinGlanceAppWidget", "Widget receiver not found for id: $widgetId", e)
+ return
+ }
+
+ val glanceManager = GlanceAppWidgetManager(reactApplicationContext)
+ val previewSize =
+ if (options != null) {
+ val width = if (options.hasKey("previewWidth")) options.getDouble("previewWidth").toFloat() else null
+ val height = if (options.hasKey("previewHeight")) options.getDouble("previewHeight").toFloat() else null
+ if (width != null && height != null) DpSize(width.dp, height.dp) else null
+ } else {
+ null
+ }
+
+ val result =
+ runBlocking {
+ if (previewSize != null) {
+ val previewWidget = VoltraGlanceWidget(widgetId)
+ glanceManager.requestPinGlanceAppWidget(
+ receiver = receiverClass,
+ preview = previewWidget,
+ previewState = previewSize,
+ )
+ } else {
+ glanceManager.requestPinGlanceAppWidget(receiverClass)
+ }
+ }
+
+ Log.d(TAG, "requestPinGlanceAppWidget completed with result=$result")
+ promise.resolve(result)
+ }
+
+ override fun preloadImages(
+ images: ReadableArray,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "preloadImages called with ${images.size()} images")
+ val result =
+ runBlocking {
+ (0 until images.size())
+ .mapNotNull { i -> images.getMap(i) }
+ .map { img ->
+ async {
+ val url = img.getString("url") ?: return@async Pair(null, "missing url")
+ val key = img.getString("key") ?: return@async Pair(null, "missing key")
+ val method = if (img.hasKey("method")) img.getString("method") ?: "GET" else "GET"
+
+ @Suppress("UNCHECKED_CAST")
+ val headers =
+ if (img.hasKey("headers")) {
+ img.getMap("headers")?.toHashMap()?.mapValues { it.value as String }
+ } else {
+ null
+ }
+
+ val resultKey = imageManager.preloadImage(url, key, method, headers)
+ if (resultKey != null) Pair(key, null) else Pair(key, "Failed to download image")
+ }
+ }.awaitAll()
+ }
+
+ val succeeded = result.filter { it.second == null }.mapNotNull { it.first }
+ val failed =
+ result.filter { it.second != null }.map { (key, error) ->
+ WritableNativeMap().apply {
+ putString("key", key)
+ putString("error", error)
+ }
+ }
+
+ val out =
+ WritableNativeMap().apply {
+ putArray("succeeded", Arguments.fromList(succeeded))
+ val failedArray = Arguments.createArray()
+ failed.forEach { failedArray.pushMap(it) }
+ putArray("failed", failedArray)
+ }
+ promise.resolve(out)
+ }
+
+ override fun clearPreloadedImages(
+ keys: ReadableArray?,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "clearPreloadedImages called with keys=$keys")
+ val keyList: List? =
+ keys?.let { array ->
+ (0 until array.size()).mapNotNull { array.getString(it) }
+ }
+ imageManager.clearPreloadedImages(keyList)
+ promise.resolve(null)
+ }
+
+ override fun setWidgetServerCredentials(
+ credentials: ReadableMap,
+ promise: Promise,
+ ) {
+ Log.d(TAG, "setWidgetServerCredentials called")
+ val token =
+ credentials.getString("token")
+ ?: run {
+ promise.reject("setWidgetServerCredentials", "token is required")
+ return
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val headers =
+ if (credentials.hasKey("headers")) {
+ credentials.getMap("headers")?.toHashMap()?.mapValues { it.value as String }
+ } else {
+ null
+ }
+
+ runBlocking {
+ voltra.widget.VoltraWidgetCredentialStore.saveToken(reactApplicationContext, token)
+ if (!headers.isNullOrEmpty()) {
+ voltra.widget.VoltraWidgetCredentialStore.saveHeaders(reactApplicationContext, headers)
+ }
+ }
+
+ val wm = voltra.widget.VoltraWidgetManager(reactApplicationContext)
+ runBlocking { wm.reloadAllWidgets() }
+ Log.d(TAG, "Widget server credentials saved")
+ promise.resolve(null)
+ }
+
+ override fun clearWidgetServerCredentials(promise: Promise) {
+ Log.d(TAG, "clearWidgetServerCredentials called")
+ runBlocking { voltra.widget.VoltraWidgetCredentialStore.clearAll(reactApplicationContext) }
+ val wm = voltra.widget.VoltraWidgetManager(reactApplicationContext)
+ runBlocking { wm.reloadAllWidgets() }
+ Log.d(TAG, "Widget server credentials cleared")
+ promise.resolve(null)
+ }
+
+ override fun getActiveWidgets(promise: Promise) {
+ val manager = AppWidgetManager.getInstance(reactApplicationContext)
+ val packageName = reactApplicationContext.packageName
+ val installedProviders =
+ manager.installedProviders.filter {
+ it.provider.packageName == packageName
+ }
+
+ val activeWidgets = Arguments.createArray()
+ for (providerInfo in installedProviders) {
+ val ids = manager.getAppWidgetIds(providerInfo.provider)
+ for (id in ids) {
+ val opts = manager.getAppWidgetOptions(id)
+ val minWidth = opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
+ val minHeight = opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
+ val shortClassName = providerInfo.provider.shortClassName
+ val prefix = ".widget.VoltraWidget_"
+ val suffix = "Receiver"
+ val name =
+ if (shortClassName.startsWith(prefix) && shortClassName.endsWith(suffix)) {
+ shortClassName.substring(prefix.length, shortClassName.length - suffix.length)
+ } else {
+ shortClassName
+ }
+
+ activeWidgets.pushMap(
+ WritableNativeMap().apply {
+ putString("name", name)
+ putInt("widgetId", id)
+ putString("providerClassName", shortClassName)
+ putString("label", providerInfo.loadLabel(reactApplicationContext.packageManager).toString())
+ putInt("width", minWidth)
+ putInt("height", minHeight)
+ },
+ )
+ }
+ }
+ promise.resolve(activeWidgets)
+ }
+}
diff --git a/packages/voltra/android/src/main/java/voltra/VoltraNotificationManager.kt b/packages/android-client/android/src/main/java/voltra/VoltraNotificationManager.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/VoltraNotificationManager.kt
rename to packages/android-client/android/src/main/java/voltra/VoltraNotificationManager.kt
diff --git a/packages/voltra/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt b/packages/android-client/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt
rename to packages/android-client/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt
diff --git a/packages/android-client/android/src/main/java/voltra/VoltraPackage.kt b/packages/android-client/android/src/main/java/voltra/VoltraPackage.kt
new file mode 100644
index 00000000..7f454f72
--- /dev/null
+++ b/packages/android-client/android/src/main/java/voltra/VoltraPackage.kt
@@ -0,0 +1,37 @@
+package voltra
+
+import com.facebook.react.TurboReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
+import com.facebook.react.uimanager.ViewManager
+
+class VoltraPackage : TurboReactPackage() {
+ override fun getModule(
+ name: String,
+ reactContext: ReactApplicationContext,
+ ): NativeModule? =
+ when (name) {
+ NativeVoltraAndroidSpec.NAME -> VoltraModule(reactContext)
+ else -> null
+ }
+
+ override fun getReactModuleInfoProvider() =
+ ReactModuleInfoProvider {
+ mapOf(
+ NativeVoltraAndroidSpec.NAME to
+ ReactModuleInfo(
+ NativeVoltraAndroidSpec.NAME,
+ VoltraModule::class.java.name,
+ false,
+ false,
+ false,
+ true,
+ ),
+ )
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> =
+ listOf(VoltraRNManager())
+}
diff --git a/packages/voltra/android/src/main/java/voltra/VoltraRN.kt b/packages/android-client/android/src/main/java/voltra/VoltraRN.kt
similarity index 80%
rename from packages/voltra/android/src/main/java/voltra/VoltraRN.kt
rename to packages/android-client/android/src/main/java/voltra/VoltraRN.kt
index 1c552687..402dce69 100644
--- a/packages/voltra/android/src/main/java/voltra/VoltraRN.kt
+++ b/packages/android-client/android/src/main/java/voltra/VoltraRN.kt
@@ -2,14 +2,11 @@ package voltra
import android.content.Context
import android.view.View
-import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
-import expo.modules.kotlin.AppContext
-import expo.modules.kotlin.views.ExpoView
import kotlinx.coroutines.*
import voltra.glance.GlanceFactory
import voltra.parsing.VoltraPayloadParser
@@ -18,10 +15,8 @@ import kotlin.math.abs
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
class VoltraRN(
context: Context,
- appContext: AppContext,
-) : ExpoView(context, appContext) {
+) : FrameLayout(context) {
private var mainScope: CoroutineScope? = null
- private val frameLayout = FrameLayout(context)
private var viewId: String? = null
private var payload: String? = null
private var updateJob: Job? = null
@@ -30,16 +25,6 @@ class VoltraRN(
private var lastRenderedWidthDp: Float = 0f
private var lastRenderedHeightDp: Float = 0f
- init {
- // Ensure FrameLayout takes full space
- frameLayout.layoutParams =
- FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- )
- addView(frameLayout)
- }
-
fun setViewId(id: String) {
if (this.viewId == id) return
this.viewId = id
@@ -57,15 +42,15 @@ class VoltraRN(
val id = viewId ?: return
val density = context.resources.displayMetrics.density
- val widthDp = frameLayout.width.toFloat() / density
- val heightDp = frameLayout.height.toFloat() / density
+ val widthDp = this.width.toFloat() / density
+ val heightDp = this.height.toFloat() / density
// Don't render until we have real dimensions from layout.
// onLayout() will call updateView() once dimensions are available.
if (widthDp <= 1f || heightDp <= 1f) return
// Avoid redundant updates if nothing significant changed and we already have a view
- if (frameLayout.childCount > 0 &&
+ if (this.childCount > 0 &&
payloadStr == lastRenderedPayload &&
abs(widthDp - lastRenderedWidthDp) < 1.0f &&
abs(heightDp - lastRenderedHeightDp) < 1.0f
@@ -76,6 +61,7 @@ class VoltraRN(
updateJob?.cancel()
updateJob =
mainScope?.launch {
+ val host = this@VoltraRN
try {
// Parse payload on background thread
val voltraPayload =
@@ -94,7 +80,7 @@ class VoltraRN(
?: voltraPayload.variants?.values?.firstOrNull()
if (node == null) {
- frameLayout.removeAllViews()
+ host.removeAllViews()
return@launch
}
@@ -116,8 +102,8 @@ class VoltraRN(
// Check if the frameLayout dimensions have changed since we started composing.
// If so, skip this render — a new updateView() will be triggered by onLayout.
- val currentWidthDp = frameLayout.width.toFloat() / density
- val currentHeightDp = frameLayout.height.toFloat() / density
+ val currentWidthDp = host.width.toFloat() / density
+ val currentHeightDp = host.height.toFloat() / density
if (abs(currentWidthDp - widthDp) >= 1.0f || abs(currentHeightDp - heightDp) >= 1.0f) {
return@launch
}
@@ -130,9 +116,9 @@ class VoltraRN(
// style bleed (e.g. padding from a previous widget persisting because
// the new widget's RemoteViews doesn't explicitly reset it to zero).
var applied = false
- if (frameLayout.childCount > 0 && payloadStr == lastRenderedPayload) {
+ if (host.childCount > 0 && payloadStr == lastRenderedPayload) {
try {
- val existingView = frameLayout.getChildAt(0)
+ val existingView = host.getChildAt(0)
remoteViews.reapply(context, existingView)
applied = true
} catch (e: Exception) {
@@ -141,14 +127,14 @@ class VoltraRN(
if (!applied) {
// Inflate with parent to ensure correct LayoutParams, but don't attach yet
- val inflatedView = remoteViews.apply(context, frameLayout)
+ val inflatedView = remoteViews.apply(context, host)
// Add new view FIRST, then remove old ones to prevent flickering
- frameLayout.addView(inflatedView)
+ host.addView(inflatedView)
- val childCount = frameLayout.childCount
+ val childCount = host.childCount
if (childCount > 1) {
- frameLayout.removeViews(0, childCount - 1)
+ host.removeViews(0, childCount - 1)
}
}
@@ -158,15 +144,15 @@ class VoltraRN(
// won't trigger layout automatically.
// 2) After reapply: text content or styles may have changed, requiring
// re-measurement to avoid stale layout constraints (e.g. truncated text).
- frameLayout.measure(
- View.MeasureSpec.makeMeasureSpec(frameLayout.width, View.MeasureSpec.EXACTLY),
- View.MeasureSpec.makeMeasureSpec(frameLayout.height, View.MeasureSpec.EXACTLY),
+ host.measure(
+ View.MeasureSpec.makeMeasureSpec(host.width, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(host.height, View.MeasureSpec.EXACTLY),
)
- frameLayout.layout(
- frameLayout.left,
- frameLayout.top,
- frameLayout.right,
- frameLayout.bottom,
+ host.layout(
+ host.left,
+ host.top,
+ host.right,
+ host.bottom,
)
// Update tracking state
diff --git a/packages/android-client/android/src/main/java/voltra/VoltraRNBridgeExtensions.kt b/packages/android-client/android/src/main/java/voltra/VoltraRNBridgeExtensions.kt
new file mode 100644
index 00000000..60ad7a69
--- /dev/null
+++ b/packages/android-client/android/src/main/java/voltra/VoltraRNBridgeExtensions.kt
@@ -0,0 +1,49 @@
+package voltra
+
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.WritableNativeMap
+
+fun AndroidOngoingNotificationOptions(map: ReadableMap) =
+ AndroidOngoingNotificationOptions(
+ notificationId = map.takeIf { it.hasKey("notificationId") }?.getString("notificationId"),
+ channelId = map.takeIf { it.hasKey("channelId") }?.getString("channelId"),
+ smallIcon = map.takeIf { it.hasKey("smallIcon") }?.getString("smallIcon"),
+ deepLinkUrl = map.takeIf { it.hasKey("deepLinkUrl") }?.getString("deepLinkUrl"),
+ requestPromotedOngoing =
+ map
+ .takeIf { it.hasKey("requestPromotedOngoing") }
+ ?.getBoolean("requestPromotedOngoing"),
+ fallbackBehavior = map.takeIf { it.hasKey("fallbackBehavior") }?.getString("fallbackBehavior"),
+ )
+
+fun AndroidOngoingNotificationStartResult.toWritableMap() =
+ WritableNativeMap().apply {
+ putBoolean("ok", ok)
+ putString("notificationId", notificationId)
+ action?.let { putString("action", it) }
+ reason?.let { putString("reason", it) }
+ }
+
+fun AndroidOngoingNotificationUpdateResult.toWritableMap() =
+ WritableNativeMap().apply {
+ putBoolean("ok", ok)
+ putString("notificationId", notificationId)
+ action?.let { putString("action", it) }
+ reason?.let { putString("reason", it) }
+ }
+
+fun AndroidOngoingNotificationUpsertResult.toWritableMap() =
+ WritableNativeMap().apply {
+ putBoolean("ok", ok)
+ putString("notificationId", notificationId)
+ action?.let { putString("action", it) }
+ reason?.let { putString("reason", it) }
+ }
+
+fun AndroidOngoingNotificationStopResult.toWritableMap() =
+ WritableNativeMap().apply {
+ putBoolean("ok", ok)
+ putString("notificationId", notificationId)
+ action?.let { putString("action", it) }
+ reason?.let { putString("reason", it) }
+ }
diff --git a/packages/android-client/android/src/main/java/voltra/VoltraRNManager.kt b/packages/android-client/android/src/main/java/voltra/VoltraRNManager.kt
new file mode 100644
index 00000000..9c97f4f6
--- /dev/null
+++ b/packages/android-client/android/src/main/java/voltra/VoltraRNManager.kt
@@ -0,0 +1,33 @@
+package voltra
+
+import android.content.Context
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.SimpleViewManager
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+
+class VoltraRNManager : SimpleViewManager() {
+ companion object {
+ const val NAME = "AndroidVoltraView"
+ }
+
+ override fun getName() = NAME
+
+ override fun createViewInstance(context: ThemedReactContext) = VoltraRN(context)
+
+ @ReactProp(name = "payload")
+ fun setPayload(
+ view: VoltraRN,
+ payload: String?,
+ ) {
+ if (payload != null) view.setPayload(payload)
+ }
+
+ @ReactProp(name = "viewId")
+ fun setViewId(
+ view: VoltraRN,
+ viewId: String?,
+ ) {
+ if (viewId != null) view.setViewId(viewId)
+ }
+}
diff --git a/packages/voltra/android/src/main/java/voltra/events/VoltraEvent.kt b/packages/android-client/android/src/main/java/voltra/events/VoltraEvent.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/events/VoltraEvent.kt
rename to packages/android-client/android/src/main/java/voltra/events/VoltraEvent.kt
diff --git a/packages/voltra/android/src/main/java/voltra/events/VoltraEventBus.kt b/packages/android-client/android/src/main/java/voltra/events/VoltraEventBus.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/events/VoltraEventBus.kt
rename to packages/android-client/android/src/main/java/voltra/events/VoltraEventBus.kt
diff --git a/packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt b/packages/android-client/android/src/main/java/voltra/generated/ShortNames.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt
rename to packages/android-client/android/src/main/java/voltra/generated/ShortNames.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/GlanceFactory.kt b/packages/android-client/android/src/main/java/voltra/glance/GlanceFactory.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/GlanceFactory.kt
rename to packages/android-client/android/src/main/java/voltra/glance/GlanceFactory.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt b/packages/android-client/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt
rename to packages/android-client/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt b/packages/android-client/android/src/main/java/voltra/glance/StyleUtils.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt
rename to packages/android-client/android/src/main/java/voltra/glance/StyleUtils.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/VoltraRenderContext.kt b/packages/android-client/android/src/main/java/voltra/glance/VoltraRenderContext.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/VoltraRenderContext.kt
rename to packages/android-client/android/src/main/java/voltra/glance/VoltraRenderContext.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/InputRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/InputRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/InputRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/InputRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/RenderCommon.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/RenderCommon.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/RenderCommon.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/RenderCommon.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/RendererJson.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/RendererJson.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/RendererJson.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/RendererJson.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt b/packages/android-client/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt
rename to packages/android-client/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt
diff --git a/packages/voltra/android/src/main/java/voltra/images/VoltraImageManager.kt b/packages/android-client/android/src/main/java/voltra/images/VoltraImageManager.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/images/VoltraImageManager.kt
rename to packages/android-client/android/src/main/java/voltra/images/VoltraImageManager.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/VoltraPayload.kt b/packages/android-client/android/src/main/java/voltra/models/VoltraPayload.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/VoltraPayload.kt
rename to packages/android-client/android/src/main/java/voltra/models/VoltraPayload.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidBoxParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidBoxParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidBoxParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidBoxParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidChartParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidChartParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidChartParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidChartParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCheckBoxParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCheckBoxParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCheckBoxParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCheckBoxParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCircleIconButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCircleIconButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCircleIconButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCircleIconButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCircularProgressIndicatorParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCircularProgressIndicatorParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidCircularProgressIndicatorParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidCircularProgressIndicatorParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidColumnParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidColumnParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidColumnParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidColumnParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidFilledButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidFilledButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidFilledButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidFilledButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidImageParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidImageParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidImageParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidImageParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLazyColumnParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLazyColumnParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLazyColumnParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLazyColumnParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLazyVerticalGridParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLazyVerticalGridParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLazyVerticalGridParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLazyVerticalGridParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLinearProgressIndicatorParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLinearProgressIndicatorParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidLinearProgressIndicatorParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidLinearProgressIndicatorParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidOutlineButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidOutlineButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidOutlineButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidOutlineButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidRadioButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidRadioButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidRadioButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidRadioButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidRowParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidRowParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidRowParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidRowParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidScaffoldParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidScaffoldParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidScaffoldParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidScaffoldParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSpacerParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSpacerParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSpacerParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSpacerParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSquareIconButtonParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSquareIconButtonParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSquareIconButtonParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSquareIconButtonParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSwitchParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSwitchParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidSwitchParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidSwitchParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidTextParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidTextParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidTextParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidTextParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/models/parameters/AndroidTitleBarParameters.kt b/packages/android-client/android/src/main/java/voltra/models/parameters/AndroidTitleBarParameters.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/models/parameters/AndroidTitleBarParameters.kt
rename to packages/android-client/android/src/main/java/voltra/models/parameters/AndroidTitleBarParameters.kt
diff --git a/packages/voltra/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt b/packages/android-client/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt
rename to packages/android-client/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt
diff --git a/packages/voltra/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt b/packages/android-client/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt
rename to packages/android-client/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt
diff --git a/packages/voltra/android/src/main/java/voltra/parsing/VoltraDecompressor.kt b/packages/android-client/android/src/main/java/voltra/parsing/VoltraDecompressor.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/parsing/VoltraDecompressor.kt
rename to packages/android-client/android/src/main/java/voltra/parsing/VoltraDecompressor.kt
diff --git a/packages/voltra/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt b/packages/android-client/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt
rename to packages/android-client/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt
diff --git a/packages/voltra/android/src/main/java/voltra/parsing/VoltraSerializers.kt b/packages/android-client/android/src/main/java/voltra/parsing/VoltraSerializers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/parsing/VoltraSerializers.kt
rename to packages/android-client/android/src/main/java/voltra/parsing/VoltraSerializers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/payload/ComponentTypeID.kt b/packages/android-client/android/src/main/java/voltra/payload/ComponentTypeID.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/payload/ComponentTypeID.kt
rename to packages/android-client/android/src/main/java/voltra/payload/ComponentTypeID.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/JSColorParser.kt b/packages/android-client/android/src/main/java/voltra/styling/JSColorParser.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/JSColorParser.kt
rename to packages/android-client/android/src/main/java/voltra/styling/JSColorParser.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/JSStyleParser.kt b/packages/android-client/android/src/main/java/voltra/styling/JSStyleParser.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/JSStyleParser.kt
rename to packages/android-client/android/src/main/java/voltra/styling/JSStyleParser.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/StyleConverter.kt b/packages/android-client/android/src/main/java/voltra/styling/StyleConverter.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/StyleConverter.kt
rename to packages/android-client/android/src/main/java/voltra/styling/StyleConverter.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/StyleModifiers.kt b/packages/android-client/android/src/main/java/voltra/styling/StyleModifiers.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/StyleModifiers.kt
rename to packages/android-client/android/src/main/java/voltra/styling/StyleModifiers.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/StyleStructures.kt b/packages/android-client/android/src/main/java/voltra/styling/StyleStructures.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/StyleStructures.kt
rename to packages/android-client/android/src/main/java/voltra/styling/StyleStructures.kt
diff --git a/packages/voltra/android/src/main/java/voltra/styling/VoltraColorValue.kt b/packages/android-client/android/src/main/java/voltra/styling/VoltraColorValue.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/styling/VoltraColorValue.kt
rename to packages/android-client/android/src/main/java/voltra/styling/VoltraColorValue.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraCryptoManager.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraCryptoManager.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraCryptoManager.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraCryptoManager.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraGlanceWidget.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraGlanceWidget.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraGlanceWidget.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraGlanceWidget.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetCredentialStore.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetCredentialStore.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetCredentialStore.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetCredentialStore.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetManager.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetManager.kt
similarity index 99%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetManager.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetManager.kt
index 0626c748..a5a2543e 100644
--- a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetManager.kt
+++ b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetManager.kt
@@ -28,7 +28,7 @@ class VoltraWidgetManager(
private const val KEY_DEEP_LINK_PREFIX = "Voltra_Widget_DeepLinkURL_"
private const val ASSET_INITIAL_STATES = "voltra_initial_states.json"
- /** Keys must match `packages/expo-plugin/src/android/files/initialStates.ts` */
+ /** Keys must match `@use-voltra/android-client` expo-plugin `initialStates.ts` */
private const val LOCALIZED_INITIAL_STATE_KEY = "__voltraLocales"
}
@@ -137,7 +137,7 @@ class VoltraWidgetManager(
private fun normalizeLocaleTag(tag: String): String = tag.trim().lowercase().replace('_', '-')
- /** Mirrors `packages/expo-plugin/src/utils/localePick.ts` */
+ /** Mirrors `@use-voltra/expo-plugin` `localePick` */
private fun pickLocalizedPayload(
perLocale: JSONObject,
res: Resources,
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetReceiver.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetReceiver.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetReceiver.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetReceiver.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateScheduler.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateScheduler.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateScheduler.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateScheduler.kt
diff --git a/packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt b/packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt
similarity index 100%
rename from packages/voltra/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt
rename to packages/android-client/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt
diff --git a/packages/voltra/android/src/main/res/xml/voltra_file_paths.xml b/packages/android-client/android/src/main/res/xml/voltra_file_paths.xml
similarity index 100%
rename from packages/voltra/android/src/main/res/xml/voltra_file_paths.xml
rename to packages/android-client/android/src/main/res/xml/voltra_file_paths.xml
diff --git a/packages/voltra/android/src/test/java/voltra/SmokeTest.kt b/packages/android-client/android/src/test/java/voltra/SmokeTest.kt
similarity index 100%
rename from packages/voltra/android/src/test/java/voltra/SmokeTest.kt
rename to packages/android-client/android/src/test/java/voltra/SmokeTest.kt
diff --git a/packages/voltra/android/src/test/java/voltra/glance/renderers/RendererJsonTest.kt b/packages/android-client/android/src/test/java/voltra/glance/renderers/RendererJsonTest.kt
similarity index 100%
rename from packages/voltra/android/src/test/java/voltra/glance/renderers/RendererJsonTest.kt
rename to packages/android-client/android/src/test/java/voltra/glance/renderers/RendererJsonTest.kt
diff --git a/packages/voltra/android/src/test/java/voltra/parsing/VoltraPayloadParserTest.kt b/packages/android-client/android/src/test/java/voltra/parsing/VoltraPayloadParserTest.kt
similarity index 100%
rename from packages/voltra/android/src/test/java/voltra/parsing/VoltraPayloadParserTest.kt
rename to packages/android-client/android/src/test/java/voltra/parsing/VoltraPayloadParserTest.kt
diff --git a/packages/android-client/expo-plugin/app.plugin.js b/packages/android-client/expo-plugin/app.plugin.js
new file mode 100644
index 00000000..8cb3b172
--- /dev/null
+++ b/packages/android-client/expo-plugin/app.plugin.js
@@ -0,0 +1,3 @@
+const plugin = require('./build/cjs/index.js')
+
+module.exports = plugin.default ?? plugin
diff --git a/packages/android-client/expo-plugin/jest.config.js b/packages/android-client/expo-plugin/jest.config.js
new file mode 100644
index 00000000..5835d68b
--- /dev/null
+++ b/packages/android-client/expo-plugin/jest.config.js
@@ -0,0 +1,18 @@
+/** @type {import('jest').Config} */
+module.exports = {
+ testEnvironment: 'node',
+ testMatch: ['/src/**/*.node.test.ts'],
+ modulePathIgnorePatterns: ['/build'],
+ moduleNameMapper: {
+ '^@use-voltra/expo-plugin$': '/../../expo-plugin/src/index.ts',
+ '^@use-voltra/expo-plugin/(.*)$': '/../../expo-plugin/src/$1',
+ },
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ tsconfig: '/tsconfig.jest.json',
+ },
+ ],
+ },
+}
diff --git a/packages/expo-plugin/src/android/files/assets.ts b/packages/android-client/expo-plugin/src/android/files/assets.ts
similarity index 98%
rename from packages/expo-plugin/src/android/files/assets.ts
rename to packages/android-client/expo-plugin/src/android/files/assets.ts
index e892416f..729f1b53 100644
--- a/packages/expo-plugin/src/android/files/assets.ts
+++ b/packages/android-client/expo-plugin/src/android/files/assets.ts
@@ -2,9 +2,10 @@ import * as fs from 'fs'
import * as path from 'path'
import { vdConvert } from 'vd-tool'
-import { DEFAULT_ANDROID_USER_IMAGES_PATH, MAX_IMAGE_SIZE_BYTES } from '../../constants'
+import { MAX_IMAGE_SIZE_BYTES, logger } from '@use-voltra/expo-plugin'
+
+import { DEFAULT_ANDROID_USER_IMAGES_PATH } from '../../constants'
import type { AndroidWidgetConfig } from '../../types'
-import { logger } from '../../utils/logger'
export interface GenerateAndroidAssetsOptions {
platformProjectRoot: string
diff --git a/packages/expo-plugin/src/android/files/fonts.ts b/packages/android-client/expo-plugin/src/android/files/fonts.ts
similarity index 93%
rename from packages/expo-plugin/src/android/files/fonts.ts
rename to packages/android-client/expo-plugin/src/android/files/fonts.ts
index b7fb0e94..4d6dcaea 100644
--- a/packages/expo-plugin/src/android/files/fonts.ts
+++ b/packages/android-client/expo-plugin/src/android/files/fonts.ts
@@ -8,8 +8,7 @@
import * as fs from 'fs'
import * as path from 'path'
-import { resolveFontPaths } from '../../utils/fonts'
-import { logger } from '../../utils/logger'
+import { logger, resolveFontPaths } from '@use-voltra/expo-plugin'
export interface CopyAndroidFontsOptions {
platformProjectRoot: string
diff --git a/packages/expo-plugin/src/android/files/index.ts b/packages/android-client/expo-plugin/src/android/files/index.ts
similarity index 100%
rename from packages/expo-plugin/src/android/files/index.ts
rename to packages/android-client/expo-plugin/src/android/files/index.ts
diff --git a/packages/expo-plugin/src/android/files/initialStates.ts b/packages/android-client/expo-plugin/src/android/files/initialStates.ts
similarity index 87%
rename from packages/expo-plugin/src/android/files/initialStates.ts
rename to packages/android-client/expo-plugin/src/android/files/initialStates.ts
index 3f4512ac..b8ecc551 100644
--- a/packages/expo-plugin/src/android/files/initialStates.ts
+++ b/packages/android-client/expo-plugin/src/android/files/initialStates.ts
@@ -2,9 +2,7 @@ import fs from 'fs'
import path from 'path'
import type { AndroidWidgetConfig } from '../../types'
-import { logger } from '../../utils/logger'
-import type { PrerenderedWidgetStates } from '../../utils/prerender'
-import { prerenderWidgetState } from '../../utils/prerender'
+import { logger, prerenderWidgetState, type PrerenderedWidgetStates } from '@use-voltra/expo-plugin'
/** Wrapped asset shape when multiple locales are built; matches Android reader in VoltraWidgetManager */
export const VOLTRA_LOCALIZED_INITIAL_STATE_KEY = '__voltraLocales'
@@ -26,9 +24,8 @@ type RenderAndroidWidgetToString = (variants: unknown) => string
export async function generateAndroidInitialStates(options: GenerateInitialStatesOptions): Promise {
const { widgets, projectRoot, platformProjectRoot } = options
- // Dynamic import for ESM module compatibility
- // voltra/android/server is an ESM module, but the plugin is compiled to CommonJS
- const androidServerModuleId = 'voltra/android/server'
+ // Dynamic import keeps the plugin CommonJS-compatible while resolving the current package entry.
+ const androidServerModuleId = '@use-voltra/android/server'
const { renderAndroidWidgetToString } = (await import(androidServerModuleId)) as {
renderAndroidWidgetToString: RenderAndroidWidgetToString
}
diff --git a/packages/expo-plugin/src/android/files/kotlin.ts b/packages/android-client/expo-plugin/src/android/files/kotlin.ts
similarity index 98%
rename from packages/expo-plugin/src/android/files/kotlin.ts
rename to packages/android-client/expo-plugin/src/android/files/kotlin.ts
index 10f1cfc4..bff40b0a 100644
--- a/packages/expo-plugin/src/android/files/kotlin.ts
+++ b/packages/android-client/expo-plugin/src/android/files/kotlin.ts
@@ -3,7 +3,7 @@ import * as fs from 'fs'
import * as path from 'path'
import type { AndroidWidgetConfig } from '../../types'
-import { widgetLabelEnglish } from '../../utils/widgetLabel'
+import { widgetLabelEnglish } from '@use-voltra/expo-plugin'
export interface GenerateKotlinFilesProps {
platformProjectRoot: string
diff --git a/packages/expo-plugin/src/android/files/xml.node.test.ts b/packages/android-client/expo-plugin/src/android/files/xml.node.test.ts
similarity index 100%
rename from packages/expo-plugin/src/android/files/xml.node.test.ts
rename to packages/android-client/expo-plugin/src/android/files/xml.node.test.ts
diff --git a/packages/expo-plugin/src/android/files/xml.ts b/packages/android-client/expo-plugin/src/android/files/xml.ts
similarity index 99%
rename from packages/expo-plugin/src/android/files/xml.ts
rename to packages/android-client/expo-plugin/src/android/files/xml.ts
index dc9a9514..54cafa00 100644
--- a/packages/expo-plugin/src/android/files/xml.ts
+++ b/packages/android-client/expo-plugin/src/android/files/xml.ts
@@ -2,9 +2,9 @@ import dedent from 'dedent'
import * as fs from 'fs'
import * as path from 'path'
+import { isWidgetLocalizedMap, logger, widgetLabelEnglish } from '@use-voltra/expo-plugin'
+
import type { AndroidWidgetConfig } from '../../types'
-import { logger } from '../../utils/logger'
-import { isWidgetLocalizedMap, widgetLabelEnglish } from '../../utils/widgetLabel'
export interface GenerateXmlFilesProps {
platformProjectRoot: string
diff --git a/packages/android-client/expo-plugin/src/android/index.ts b/packages/android-client/expo-plugin/src/android/index.ts
new file mode 100644
index 00000000..311f6d79
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/android/index.ts
@@ -0,0 +1,28 @@
+import { ConfigPlugin, withPlugins } from '@expo/config-plugins'
+
+import type { AndroidPluginProps } from '../types'
+import { validateAndroidWidgetConfig } from '../validation'
+import { generateAndroidWidgetFiles } from './files'
+import { configureAndroidManifest } from './manifest'
+
+/**
+ * Orchestrates Android widget file generation and AndroidManifest configuration.
+ */
+export const withAndroid: ConfigPlugin = (config, props) => {
+ const { enableNotifications, widgets, userImagesPath, fonts } = props
+
+ if (!config.android?.package) {
+ throw new Error(
+ 'Voltra Android config plugin requires expo.android.package to be set in app.json/app.config.* to configure Android widgets.'
+ )
+ }
+
+ const projectRoot = (config as { modRequest?: { projectRoot?: string } }).modRequest?.projectRoot
+
+ widgets.forEach((widget) => validateAndroidWidgetConfig(widget, projectRoot))
+
+ return withPlugins(config, [
+ [generateAndroidWidgetFiles, { widgets, userImagesPath, fonts }],
+ [configureAndroidManifest, { enableNotifications, widgets }],
+ ])
+}
diff --git a/packages/expo-plugin/src/android/manifest.ts b/packages/android-client/expo-plugin/src/android/manifest.ts
similarity index 100%
rename from packages/expo-plugin/src/android/manifest.ts
rename to packages/android-client/expo-plugin/src/android/manifest.ts
diff --git a/packages/android-client/expo-plugin/src/constants.ts b/packages/android-client/expo-plugin/src/constants.ts
new file mode 100644
index 00000000..8b1a04a9
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/constants.ts
@@ -0,0 +1,2 @@
+/** Default path for user-provided Android widget images */
+export const DEFAULT_ANDROID_USER_IMAGES_PATH = './assets/voltra-android'
diff --git a/packages/android-client/expo-plugin/src/index.ts b/packages/android-client/expo-plugin/src/index.ts
new file mode 100644
index 00000000..4ba2beb0
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/index.ts
@@ -0,0 +1,34 @@
+import { withAndroid } from './android'
+import type { AndroidConfigPluginProps, VoltraAndroidConfigPlugin } from './types'
+import { validateAndroidConfigPluginProps } from './validation'
+
+/**
+ * Voltra Android Expo config plugin.
+ *
+ * Configures home screen widgets and optional notification manifest entries.
+ */
+const withVoltraAndroid: VoltraAndroidConfigPlugin = (config, props = {}) => {
+ const projectRoot = (config as { modRequest?: { projectRoot?: string } }).modRequest?.projectRoot
+ validateAndroidConfigPluginProps(props, projectRoot)
+
+ const widgets = props.widgets ?? []
+ if (widgets.length === 0 && !props.enableNotifications) {
+ return config
+ }
+
+ return withAndroid(config, {
+ enableNotifications: props.enableNotifications,
+ widgets,
+ ...(props.fonts ? { fonts: props.fonts } : {}),
+ })
+}
+
+export default withVoltraAndroid
+
+export type {
+ AndroidConfigPluginProps,
+ AndroidPluginProps,
+ AndroidWidgetConfig,
+ AndroidWidgetServerUpdateConfig,
+ VoltraAndroidConfigPlugin,
+} from './types'
diff --git a/packages/android-client/expo-plugin/src/types.ts b/packages/android-client/expo-plugin/src/types.ts
new file mode 100644
index 00000000..e173f0b0
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/types.ts
@@ -0,0 +1,53 @@
+import type { ConfigPlugin } from '@expo/config-plugins'
+
+import type { WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin'
+
+/**
+ * Configuration for a single Android home screen widget.
+ */
+export interface AndroidWidgetConfig {
+ id: string
+ displayName: WidgetLabel
+ description: WidgetLabel
+ minWidth?: number
+ minHeight?: number
+ minCellWidth?: number
+ minCellHeight?: number
+ targetCellWidth: number
+ targetCellHeight: number
+ resizeMode?: 'none' | 'horizontal' | 'vertical' | 'horizontal|vertical'
+ widgetCategory?: 'home_screen' | 'keyguard' | 'home_screen|keyguard'
+ initialStatePath?: WidgetInitialStatePath
+ serverUpdate?: AndroidWidgetServerUpdateConfig
+ previewImage?: string
+ previewLayout?: string
+}
+
+/**
+ * Server-driven Android widget updates (WorkManager).
+ */
+export interface AndroidWidgetServerUpdateConfig {
+ url: string
+ /** @default 60 */
+ intervalMinutes?: number
+ /** @default false */
+ refresh?: boolean
+}
+
+/**
+ * Options for `@use-voltra/android-client` Expo config plugin.
+ */
+export interface AndroidConfigPluginProps {
+ enableNotifications?: boolean
+ widgets?: AndroidWidgetConfig[]
+ fonts?: string[]
+}
+
+export type VoltraAndroidConfigPlugin = ConfigPlugin
+
+export interface AndroidPluginProps {
+ enableNotifications?: boolean
+ widgets: AndroidWidgetConfig[]
+ userImagesPath?: string
+ fonts?: string[]
+}
diff --git a/packages/android-client/expo-plugin/src/validation.node.test.ts b/packages/android-client/expo-plugin/src/validation.node.test.ts
new file mode 100644
index 00000000..6b0ca687
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/validation.node.test.ts
@@ -0,0 +1,42 @@
+import { validateAndroidConfigPluginProps } from './validation'
+
+describe('validateAndroidConfigPluginProps', () => {
+ it('accepts widgets with required cell sizes', () => {
+ expect(() =>
+ validateAndroidConfigPluginProps({
+ widgets: [
+ {
+ id: 'demo',
+ displayName: 'Demo',
+ description: 'Demo widget',
+ targetCellWidth: 2,
+ targetCellHeight: 2,
+ },
+ ],
+ })
+ ).not.toThrow()
+ })
+
+ it('rejects duplicate widget ids', () => {
+ expect(() =>
+ validateAndroidConfigPluginProps({
+ widgets: [
+ {
+ id: 'demo',
+ displayName: 'Demo',
+ description: 'One',
+ targetCellWidth: 2,
+ targetCellHeight: 2,
+ },
+ {
+ id: 'demo',
+ displayName: 'Demo',
+ description: 'Two',
+ targetCellWidth: 2,
+ targetCellHeight: 2,
+ },
+ ],
+ })
+ ).toThrow(/Duplicate Android widget ID/)
+ })
+})
diff --git a/packages/android-client/expo-plugin/src/validation.ts b/packages/android-client/expo-plugin/src/validation.ts
new file mode 100644
index 00000000..40124711
--- /dev/null
+++ b/packages/android-client/expo-plugin/src/validation.ts
@@ -0,0 +1,102 @@
+import * as fs from 'fs'
+import * as path from 'path'
+
+import { validateHomeScreenWidgetId, validateInitialStatePath, validateWidgetLabel } from '@use-voltra/expo-plugin'
+
+import type { AndroidConfigPluginProps, AndroidWidgetConfig } from './types'
+
+export function validateAndroidWidgetConfig(widget: AndroidWidgetConfig, projectRoot?: string): void {
+ validateHomeScreenWidgetId(widget.id)
+ validateWidgetLabel(widget.displayName, widget.id, 'displayName')
+ validateWidgetLabel(widget.description, widget.id, 'description')
+ validateInitialStatePath(widget.initialStatePath, widget.id, projectRoot)
+
+ if (typeof widget.targetCellWidth !== 'number') {
+ throw new Error(`Widget '${widget.id}': targetCellWidth is required and must be a number`)
+ }
+ if (!Number.isInteger(widget.targetCellWidth) || widget.targetCellWidth < 1) {
+ throw new Error(`Widget '${widget.id}': targetCellWidth must be a positive integer (typically 1-5)`)
+ }
+
+ if (typeof widget.targetCellHeight !== 'number') {
+ throw new Error(`Widget '${widget.id}': targetCellHeight is required and must be a number`)
+ }
+ if (!Number.isInteger(widget.targetCellHeight) || widget.targetCellHeight < 1) {
+ throw new Error(`Widget '${widget.id}': targetCellHeight must be a positive integer (typically 1-5)`)
+ }
+
+ if (widget.minCellWidth !== undefined) {
+ if (typeof widget.minCellWidth !== 'number' || !Number.isInteger(widget.minCellWidth) || widget.minCellWidth < 1) {
+ throw new Error(`Widget '${widget.id}': minCellWidth must be a positive integer`)
+ }
+ }
+
+ if (widget.minCellHeight !== undefined) {
+ if (
+ typeof widget.minCellHeight !== 'number' ||
+ !Number.isInteger(widget.minCellHeight) ||
+ widget.minCellHeight < 1
+ ) {
+ throw new Error(`Widget '${widget.id}': minCellHeight must be a positive integer`)
+ }
+ }
+
+ if (widget.previewImage !== undefined) {
+ if (typeof widget.previewImage !== 'string' || !widget.previewImage.trim()) {
+ throw new Error(`Widget '${widget.id}': previewImage must be a non-empty string`)
+ }
+
+ const ext = path.extname(widget.previewImage).toLowerCase()
+ const validImageExts = ['.png', '.jpg', '.jpeg', '.webp']
+ if (!validImageExts.includes(ext)) {
+ throw new Error(`Widget '${widget.id}': previewImage must be a PNG, JPG, JPEG, or WebP file. Got: ${ext}`)
+ }
+
+ if (projectRoot) {
+ const fullPath = path.join(projectRoot, widget.previewImage)
+ if (!fs.existsSync(fullPath)) {
+ throw new Error(`Widget '${widget.id}': previewImage file not found at ${widget.previewImage}`)
+ }
+ }
+ }
+
+ if (widget.previewLayout !== undefined) {
+ if (typeof widget.previewLayout !== 'string' || !widget.previewLayout.trim()) {
+ throw new Error(`Widget '${widget.id}': previewLayout must be a non-empty string`)
+ }
+
+ const ext = path.extname(widget.previewLayout).toLowerCase()
+ if (ext !== '.xml') {
+ throw new Error(`Widget '${widget.id}': previewLayout must be an XML file. Got: ${ext}`)
+ }
+
+ if (projectRoot) {
+ const fullPath = path.join(projectRoot, widget.previewLayout)
+ if (!fs.existsSync(fullPath)) {
+ throw new Error(`Widget '${widget.id}': previewLayout file not found at ${widget.previewLayout}`)
+ }
+ }
+ }
+}
+
+export function validateAndroidConfigPluginProps(props: AndroidConfigPluginProps, projectRoot?: string): void {
+ if (props.enableNotifications !== undefined && typeof props.enableNotifications !== 'boolean') {
+ throw new Error('enableNotifications must be a boolean')
+ }
+
+ if (props.widgets !== undefined) {
+ if (!Array.isArray(props.widgets)) {
+ throw new Error('widgets must be an array')
+ }
+
+ const seenIds = new Set()
+ for (const widget of props.widgets) {
+ validateAndroidWidgetConfig(widget, projectRoot)
+
+ if (seenIds.has(widget.id)) {
+ throw new Error(`Duplicate Android widget ID: '${widget.id}'`)
+ }
+ seenIds.add(widget.id)
+ }
+ }
+}
diff --git a/packages/android-client/expo-plugin/tsconfig.base.json b/packages/android-client/expo-plugin/tsconfig.base.json
new file mode 100644
index 00000000..6bb57ecd
--- /dev/null
+++ b/packages/android-client/expo-plugin/tsconfig.base.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020"],
+ "rootDir": "./src",
+ "moduleResolution": "node",
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "include": ["./src"],
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*", "**/*.node.test.ts"]
+}
diff --git a/packages/android-client/tsconfig.cjs.json b/packages/android-client/expo-plugin/tsconfig.cjs.json
similarity index 100%
rename from packages/android-client/tsconfig.cjs.json
rename to packages/android-client/expo-plugin/tsconfig.cjs.json
diff --git a/packages/android-client/tsconfig.esm.json b/packages/android-client/expo-plugin/tsconfig.esm.json
similarity index 100%
rename from packages/android-client/tsconfig.esm.json
rename to packages/android-client/expo-plugin/tsconfig.esm.json
diff --git a/packages/android-client/expo-plugin/tsconfig.jest.json b/packages/android-client/expo-plugin/tsconfig.jest.json
new file mode 100644
index 00000000..c0c766d0
--- /dev/null
+++ b/packages/android-client/expo-plugin/tsconfig.jest.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "types": ["jest", "node"]
+ },
+ "include": ["./src/**/*.ts"]
+}
diff --git a/packages/android-client/expo-plugin/tsconfig.typecheck.json b/packages/android-client/expo-plugin/tsconfig.typecheck.json
new file mode 100644
index 00000000..58e6f899
--- /dev/null
+++ b/packages/android-client/expo-plugin/tsconfig.typecheck.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "module": "ES2020",
+ "rootDir": "../../..",
+ "baseUrl": "../../..",
+ "paths": {
+ "@use-voltra/expo-plugin": ["packages/expo-plugin/src/index.ts"]
+ }
+ },
+ "include": ["./src"]
+}
diff --git a/packages/android-client/tsconfig.types.json b/packages/android-client/expo-plugin/tsconfig.types.json
similarity index 100%
rename from packages/android-client/tsconfig.types.json
rename to packages/android-client/expo-plugin/tsconfig.types.json
diff --git a/packages/android-client/package.json b/packages/android-client/package.json
index d52d80b6..40d5768b 100644
--- a/packages/android-client/package.json
+++ b/packages/android-client/package.json
@@ -2,30 +2,62 @@
"name": "@use-voltra/android-client",
"version": "1.4.1",
"description": "Client-only Voltra APIs for Android",
- "main": "build/cjs/index.js",
- "module": "build/esm/index.js",
- "types": "build/types/index.d.ts",
+ "main": "./build/commonjs/index.js",
+ "module": "./build/module/index.js",
+ "types": "./build/typescript/module/index.d.ts",
"exports": {
".": {
- "types": "./build/types/index.d.ts",
- "require": "./build/cjs/index.js",
- "import": "./build/esm/index.js",
- "default": "./build/esm/index.js"
+ "source": "./src/index.ts",
+ "import": {
+ "types": "./build/typescript/module/index.d.ts",
+ "default": "./build/module/index.js"
+ },
+ "require": {
+ "types": "./build/typescript/commonjs/index.d.ts",
+ "default": "./build/commonjs/index.js"
+ },
+ "default": "./build/module/index.js"
},
- "./package.json": "./package.json"
+ "./package.json": "./package.json",
+ "./app.plugin.js": "./expo-plugin/app.plugin.js"
},
"files": [
+ "android",
"build",
+ "expo-plugin",
"README.md"
],
"scripts": {
- "build": "node ../../scripts/build-package.mjs packages/android-client",
- "clean": "rm -rf build",
- "lint": "oxlint src",
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
+ "build": "npm run clean && npm run build:expo-plugin && bob build",
+ "build:expo-plugin": "node ../../scripts/build-package.mjs packages/android-client/expo-plugin",
+ "clean": "rm -rf build && npm run clean:expo-plugin",
+ "clean:expo-plugin": "rm -rf expo-plugin/build",
+ "format:check": "npm run format:kotlin:check",
+ "format:fix": "npm run format:kotlin:fix",
+ "format:kotlin:check": "ktlint \"android/**/*.kt\"",
+ "format:kotlin:fix": "ktlint -F \"android/**/*.kt\"",
+ "lint": "oxlint src expo-plugin/src",
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit && tsc -p expo-plugin/tsconfig.typecheck.json --noEmit",
+ "test": "jest --config expo-plugin/jest.config.js",
+ "test:kotlin": "cd ../../example/android && ./gradlew :use-voltra_android-client:testDebugUnitTest"
},
"dependencies": {
- "@use-voltra/android": "1.4.1"
+ "@babel/core": "^7.27.4",
+ "@expo/config-plugins": "~10.1.2",
+ "@use-voltra/android": "1.4.1",
+ "@use-voltra/expo-plugin": "1.4.1",
+ "vd-tool": "^4.0.2"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.14",
+ "@types/node": "^20.19.25",
+ "jest": "^29.7.0",
+ "ts-jest": "^29.3.4"
},
"keywords": [
"react-native",
@@ -45,9 +77,39 @@
},
"license": "MIT",
"homepage": "https://use-voltra.dev",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "build",
+ "exclude": "**/{__tests__,__fixtures__,__mocks__,__rsc_tests__}/**",
+ "targets": [
+ [
+ "module",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "commonjs",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ },
+ "codegenConfig": {
+ "name": "VoltraSpec",
+ "type": "all",
+ "jsSrcsDir": "src",
+ "android": {
+ "javaPackageName": "voltra"
+ }
}
}
diff --git a/packages/android-client/src/VoltraModule.ts b/packages/android-client/src/VoltraModule.ts
index 2e859e20..7b75e45d 100644
--- a/packages/android-client/src/VoltraModule.ts
+++ b/packages/android-client/src/VoltraModule.ts
@@ -1,84 +1,2 @@
-import { requireNativeModule } from 'expo'
-
-import type {
- StartAndroidOngoingNotificationOptions,
- UpdateAndroidOngoingNotificationOptions,
-} from '@use-voltra/android'
-import type { EventSubscription, PreloadImageOptions, PreloadImagesResult, WidgetServerCredentials } from './types.js'
-
-export interface VoltraAndroidModuleSpec {
- startAndroidLiveUpdate(payload: string, options: { updateName?: string; channelId?: string }): Promise
- updateAndroidLiveUpdate(notificationId: string, payload: string): Promise
- stopAndroidLiveUpdate(notificationId: string): Promise
- isAndroidLiveUpdateActive(updateName: string): boolean
- endAllAndroidLiveUpdates(): Promise
- startAndroidOngoingNotification(
- payload: string,
- options: StartAndroidOngoingNotificationOptions
- ): Promise<{
- ok: boolean
- notificationId: string
- action?: 'started'
- reason?: 'already_exists'
- }>
- upsertAndroidOngoingNotification(
- payload: string,
- options: StartAndroidOngoingNotificationOptions
- ): Promise<{
- ok: boolean
- notificationId: string
- action?: 'started' | 'updated'
- reason?: 'already_exists' | 'dismissed'
- }>
- updateAndroidOngoingNotification(
- notificationId: string,
- payload: string,
- options?: UpdateAndroidOngoingNotificationOptions
- ): Promise<{
- ok: boolean
- notificationId: string
- action?: 'updated'
- reason?: 'dismissed' | 'not_found'
- }>
- stopAndroidOngoingNotification(notificationId: string): Promise<{
- ok: boolean
- notificationId: string
- action?: 'stopped'
- reason?: 'not_found'
- }>
- isAndroidOngoingNotificationActive(notificationId: string): boolean
- getAndroidOngoingNotificationStatus(notificationId: string): {
- isActive: boolean
- isDismissed: boolean
- isPromoted?: boolean
- hasPromotableCharacteristics?: boolean
- }
- endAllAndroidOngoingNotifications(): Promise
- canPostPromotedAndroidNotifications(): boolean
- getAndroidOngoingNotificationCapabilities(): {
- apiLevel: number
- notificationsEnabled: boolean
- supportsPromotedNotifications: boolean
- canPostPromotedNotifications: boolean
- canRequestPromotedOngoing: boolean
- }
- openAndroidNotificationSettings(): Promise
- updateAndroidWidget(widgetId: string, jsonString: string, options?: { deepLinkUrl?: string }): Promise
- reloadAndroidWidgets(widgetIds?: string[] | null): Promise
- clearAndroidWidget(widgetId: string): Promise
- clearAllAndroidWidgets(): Promise
- requestPinGlanceAppWidget(
- widgetId: string,
- options?: { previewWidth?: number; previewHeight?: number }
- ): Promise
- preloadImages(images: PreloadImageOptions[]): Promise
- clearPreloadedImages(keys?: string[] | null): Promise
- setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise
- clearWidgetServerCredentials(): Promise
- getActiveWidgets(): Promise
- addListener(event: string, listener: (event: any) => void): EventSubscription
-}
-
-const VoltraModule = requireNativeModule('VoltraModule')
-
-export default VoltraModule
+export { getNativeVoltraAndroid, type Spec } from './native/NativeVoltraAndroid.js'
+export { getNativeVoltraAndroid as getNativeVoltra } from './native/NativeVoltraAndroid.js'
diff --git a/packages/android-client/src/components/VoltraView.tsx b/packages/android-client/src/components/VoltraView.tsx
index 0e49b361..fb861d10 100644
--- a/packages/android-client/src/components/VoltraView.tsx
+++ b/packages/android-client/src/components/VoltraView.tsx
@@ -1,23 +1,57 @@
-import { requireNativeView } from 'expo'
-import React, { type ReactNode, useEffect, useMemo } from 'react'
-import { type StyleProp, type ViewStyle } from 'react-native'
+import { type ReactNode, useEffect, useMemo } from 'react'
+import { View, type StyleProp, type ViewStyle } from 'react-native'
import { renderAndroidViewToJson } from '@use-voltra/android'
+import VoltraRN from '../native/VoltraRNNativeComponent'
import { addVoltraListener, type VoltraInteractionEvent } from '../events.js'
-const NativeVoltraView = requireNativeView('VoltraModule')
-
const generateViewId = () => `voltra-view-android-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
+const voltraViewStyle: ViewStyle = {
+ flex: 1,
+}
+
export type VoltraViewProps = {
+ /**
+ * Unique identifier for this view instance.
+ * Used as 'source' in interaction events to identify which view triggered the event.
+ * If not provided, a unique ID will be generated automatically.
+ */
id?: string
+ /**
+ * Voltra JSX components to render
+ */
children: ReactNode
+ /**
+ * Style for the view container
+ */
style?: StyleProp
+ /**
+ * Callback when user interacts with components in the view.
+ * Events are filtered by this view's id (source).
+ */
onInteraction?: (event: VoltraInteractionEvent) => void
+ /**
+ * Test ID for the view
+ */
+ testID?: string
}
-export function VoltraView({ id, children, style, onInteraction }: VoltraViewProps) {
+/**
+ * A React Native component that renders Voltra UI using SwiftUI in the native layer.
+ * This component accepts Voltra JSX components as children and renders them as SwiftUI components.
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ * Hello World
+ *
+ *
+ * ```
+ */
+export function VoltraView({ id, testID, style, children, onInteraction }: VoltraViewProps) {
const viewId = useMemo(() => id || generateViewId(), [id])
const payload = useMemo(() => JSON.stringify(renderAndroidViewToJson(children)), [children])
@@ -40,5 +74,9 @@ export function VoltraView({ id, children, style, onInteraction }: VoltraViewPro
return () => subscription.remove()
}, [viewId, onInteraction])
- return
+ return (
+
+
+
+ )
}
diff --git a/packages/android-client/src/events.ts b/packages/android-client/src/events.ts
index db0f7c93..8a44ffb1 100644
--- a/packages/android-client/src/events.ts
+++ b/packages/android-client/src/events.ts
@@ -1,7 +1,6 @@
import { Platform } from 'react-native'
import type { EventSubscription } from './types.js'
-import VoltraModule from './VoltraModule.js'
export type BasicVoltraEvent = {
source: string
@@ -45,10 +44,13 @@ export function addVoltraListener(
event: K,
listener: (event: VoltraEventMap[K]) => void
): EventSubscription {
+ void listener
+
if (Platform.OS !== 'ios') {
console.warn(`[Voltra] Event '${event}' is only supported on iOS. Returning no-op subscription.`)
return noopSubscription
}
- return VoltraModule.addListener(event, listener)
+ console.warn(`[Voltra] Event '${event}' is only supported on iOS. Returning no-op subscription.`)
+ return noopSubscription
}
diff --git a/packages/android-client/src/index.ts b/packages/android-client/src/index.ts
index 8ae5a392..efa2cd96 100644
--- a/packages/android-client/src/index.ts
+++ b/packages/android-client/src/index.ts
@@ -1,21 +1,9 @@
export {
- endAllAndroidLiveUpdates,
- isAndroidLiveUpdateActive,
- startAndroidLiveUpdate,
- stopAndroidLiveUpdate,
- updateAndroidLiveUpdate,
- useAndroidLiveUpdate,
-} from './live-update/api.js'
-export type {
- AndroidLiveUpdateJson,
- AndroidLiveUpdateVariants,
- AndroidLiveUpdateVariantsJson,
- StartAndroidLiveUpdateOptions,
- UpdateAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateResult,
-} from './live-update/types.js'
-export { AndroidOngoingNotification, renderAndroidOngoingNotificationPayload } from '@use-voltra/android'
+ AndroidDynamicColors,
+ AndroidOngoingNotification,
+ renderAndroidOngoingNotificationPayload,
+ VoltraAndroid,
+} from '@use-voltra/android'
export {
canPostPromotedAndroidNotifications,
endAllAndroidOngoingNotifications,
diff --git a/packages/android-client/src/live-update/api.ts b/packages/android-client/src/live-update/api.ts
deleted file mode 100644
index ad9c77d6..00000000
--- a/packages/android-client/src/live-update/api.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
-
-import { renderAndroidLiveUpdateToString, type AndroidLiveUpdateVariants } from '@use-voltra/android'
-
-import VoltraModule from '../VoltraModule.js'
-import { useUpdateOnHMR } from '../utils/index.js'
-import type {
- StartAndroidLiveUpdateOptions,
- UpdateAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateResult,
-} from './types.js'
-
-export const useAndroidLiveUpdate = (
- variants: AndroidLiveUpdateVariants,
- options?: UseAndroidLiveUpdateOptions
-): UseAndroidLiveUpdateResult => {
- const [targetId, setTargetId] = useState(() => {
- if (options?.updateName) {
- return isAndroidLiveUpdateActive(options.updateName) ? options.updateName : null
- }
- return null
- })
-
- const isActive = targetId !== null
- const optionsRef = useRef(options)
- const variantsRef = useRef(variants)
- const lastUpdateOptionsRef = useRef(undefined)
-
- useEffect(() => {
- optionsRef.current = options
- }, [options])
-
- useEffect(() => {
- variantsRef.current = variants
- }, [variants])
-
- useUpdateOnHMR()
-
- const start = useCallback(async (options?: StartAndroidLiveUpdateOptions) => {
- const id = await startAndroidLiveUpdate(variantsRef.current, {
- ...optionsRef.current,
- ...options,
- })
- setTargetId(id)
- }, [])
-
- const update = useCallback(
- async (options?: UpdateAndroidLiveUpdateOptions) => {
- if (!targetId) {
- return
- }
-
- const updateOptions = { ...optionsRef.current, ...options }
- lastUpdateOptionsRef.current = updateOptions
- await updateAndroidLiveUpdate(targetId, variantsRef.current, updateOptions)
- },
- [targetId]
- )
-
- const end = useCallback(async () => {
- if (!targetId) {
- return
- }
-
- await stopAndroidLiveUpdate(targetId)
- setTargetId(null)
- }, [targetId])
-
- useEffect(() => {
- if (!options?.autoStart) {
- return
- }
-
- start()
- }, [options?.autoStart, start])
-
- useEffect(() => {
- if (!options?.autoUpdate) return
- update(lastUpdateOptionsRef.current)
- }, [options?.autoUpdate, update, variants])
-
- return {
- start,
- update,
- end,
- isActive,
- }
-}
-
-export const startAndroidLiveUpdate = async (
- variants: AndroidLiveUpdateVariants,
- options?: StartAndroidLiveUpdateOptions
-): Promise => {
- const payload = renderAndroidLiveUpdateToString(variants)
-
- const notificationId = await VoltraModule.startAndroidLiveUpdate(payload, {
- updateName: options?.updateName,
- channelId: options?.channelId || variants.channelId || 'voltra_live_updates',
- })
-
- return notificationId
-}
-
-export const updateAndroidLiveUpdate = async (
- notificationId: string,
- variants: AndroidLiveUpdateVariants,
- _options?: UpdateAndroidLiveUpdateOptions
-): Promise => {
- const payload = renderAndroidLiveUpdateToString(variants)
-
- return VoltraModule.updateAndroidLiveUpdate(notificationId, payload)
-}
-
-export const stopAndroidLiveUpdate = async (notificationId: string): Promise => {
- return VoltraModule.stopAndroidLiveUpdate(notificationId)
-}
-
-export const isAndroidLiveUpdateActive = (updateName: string): boolean => {
- return VoltraModule.isAndroidLiveUpdateActive(updateName)
-}
-
-export async function endAllAndroidLiveUpdates(): Promise {
- return VoltraModule.endAllAndroidLiveUpdates()
-}
diff --git a/packages/android-client/src/live-update/types.ts b/packages/android-client/src/live-update/types.ts
deleted file mode 100644
index 106d3877..00000000
--- a/packages/android-client/src/live-update/types.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export type {
- AndroidLiveUpdateJson,
- AndroidLiveUpdateVariants,
- AndroidLiveUpdateVariantsJson,
- StartAndroidLiveUpdateOptions,
- UpdateAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateOptions,
- UseAndroidLiveUpdateResult,
-} from '@use-voltra/android'
diff --git a/packages/android-client/src/native/NativeVoltraAndroid.ts b/packages/android-client/src/native/NativeVoltraAndroid.ts
new file mode 100644
index 00000000..d3b10c6c
--- /dev/null
+++ b/packages/android-client/src/native/NativeVoltraAndroid.ts
@@ -0,0 +1,111 @@
+import { TurboModuleRegistry } from 'react-native'
+import type { TurboModule } from 'react-native'
+
+type PreloadImageOptions = Readonly<{
+ url: string
+ key: string
+ method?: string
+ headers?: Readonly<{ [key: string]: string }>
+}>
+
+type PreloadImageFailure = Readonly<{
+ key: string
+ error: string
+}>
+
+type PreloadImagesResult = Readonly<{
+ succeeded: string[]
+ failed: PreloadImageFailure[]
+}>
+
+type WidgetServerCredentials = Readonly<{
+ token: string
+ headers?: Readonly<{ [key: string]: string }>
+}>
+
+type StartAndroidOngoingNotificationOptionsSpec = Readonly<{
+ notificationId?: string
+ channelId: string
+ smallIcon?: string
+ deepLinkUrl?: string
+ requestPromotedOngoing?: boolean
+ fallbackBehavior?: string
+}>
+
+type UpdateAndroidOngoingNotificationOptionsSpec = Readonly<{
+ channelId?: string
+ smallIcon?: string
+ deepLinkUrl?: string
+ requestPromotedOngoing?: boolean
+ fallbackBehavior?: string
+}>
+
+type AndroidOngoingNotificationResultSpec = Readonly<{
+ ok: boolean
+ notificationId: string
+ action?: string
+ reason?: string
+}>
+
+type AndroidOngoingNotificationStatusSpec = Readonly<{
+ isActive: boolean
+ isDismissed: boolean
+ isPromoted?: boolean
+ hasPromotableCharacteristics?: boolean
+}>
+
+type AndroidOngoingNotificationCapabilitiesSpec = Readonly<{
+ apiLevel: number
+ notificationsEnabled: boolean
+ supportsPromotedNotifications: boolean
+ canPostPromotedNotifications: boolean
+ canRequestPromotedOngoing: boolean
+}>
+
+type RequestPinGlanceAppWidgetOptionsSpec = Readonly<{
+ previewWidth?: number
+ previewHeight?: number
+}>
+
+export interface Spec extends TurboModule {
+ startAndroidOngoingNotification(
+ payload: string,
+ options: StartAndroidOngoingNotificationOptionsSpec
+ ): Promise
+ upsertAndroidOngoingNotification(
+ payload: string,
+ options: StartAndroidOngoingNotificationOptionsSpec
+ ): Promise
+ updateAndroidOngoingNotification(
+ notificationId: string,
+ payload: string,
+ options?: UpdateAndroidOngoingNotificationOptionsSpec
+ ): Promise
+ stopAndroidOngoingNotification(notificationId: string): Promise
+ isAndroidOngoingNotificationActive(notificationId: string): boolean
+ getAndroidOngoingNotificationStatus(notificationId: string): AndroidOngoingNotificationStatusSpec
+ endAllAndroidOngoingNotifications(): Promise
+ canPostPromotedAndroidNotifications(): boolean
+ getAndroidOngoingNotificationCapabilities(): AndroidOngoingNotificationCapabilitiesSpec
+ openAndroidNotificationSettings(): Promise
+ updateAndroidWidget(widgetId: string, jsonString: string, options?: Readonly<{ deepLinkUrl?: string }>): Promise
+ reloadAndroidWidgets(widgetIds?: string[] | null): Promise
+ clearAndroidWidget(widgetId: string): Promise
+ clearAllAndroidWidgets(): Promise
+ requestPinGlanceAppWidget(widgetId: string, options?: RequestPinGlanceAppWidgetOptionsSpec): Promise
+ preloadImages(images: PreloadImageOptions[]): Promise
+ clearPreloadedImages(keys?: string[] | null): Promise
+ setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise
+ clearWidgetServerCredentials(): Promise
+ getActiveWidgets(): Promise>
+}
+
+export function getNativeVoltraAndroid(): Spec {
+ const voltraModule = TurboModuleRegistry.get('NativeVoltraAndroid')
+
+ if (voltraModule == null) {
+ throw new Error('NativeVoltraAndroid is not available')
+ }
+
+ return voltraModule
+}
diff --git a/packages/android-client/src/native/VoltraRNNativeComponent.ts b/packages/android-client/src/native/VoltraRNNativeComponent.ts
new file mode 100644
index 00000000..08f7cd42
--- /dev/null
+++ b/packages/android-client/src/native/VoltraRNNativeComponent.ts
@@ -0,0 +1,9 @@
+import type { HostComponent, ViewProps } from 'react-native'
+import { codegenNativeComponent } from 'react-native'
+
+export interface NativeProps extends ViewProps {
+ payload: string
+ viewId: string
+}
+
+export default codegenNativeComponent('AndroidVoltraView') as HostComponent
diff --git a/packages/android-client/src/ongoing-notification/api.ts b/packages/android-client/src/ongoing-notification/api.ts
index 06cddbe8..fe969b2e 100644
--- a/packages/android-client/src/ongoing-notification/api.ts
+++ b/packages/android-client/src/ongoing-notification/api.ts
@@ -20,7 +20,7 @@ import {
} from '@use-voltra/android'
import { useUpdateOnHMR } from '../utils/index.js'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltraAndroid } from '../native/NativeVoltraAndroid.js'
const NOTIFICATION_PERMISSION = PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
@@ -202,7 +202,7 @@ export const startAndroidOngoingNotification = async (
input: AndroidOngoingNotificationInput,
options: StartAndroidOngoingNotificationOptions
): Promise => {
- return (await VoltraModule.startAndroidOngoingNotification(
+ return (await getNativeVoltraAndroid().startAndroidOngoingNotification(
serializeAndroidOngoingNotificationInput(input),
getStartAndroidOngoingNotificationOptions(input, options)
)) as AndroidOngoingNotificationStartResult
@@ -212,7 +212,7 @@ export const upsertAndroidOngoingNotification = async (
input: AndroidOngoingNotificationInput,
options: StartAndroidOngoingNotificationOptions
): Promise => {
- return (await VoltraModule.upsertAndroidOngoingNotification(
+ return (await getNativeVoltraAndroid().upsertAndroidOngoingNotification(
serializeAndroidOngoingNotificationInput(input),
getStartAndroidOngoingNotificationOptions(input, options)
)) as AndroidOngoingNotificationUpsertResult
@@ -223,7 +223,7 @@ export const updateAndroidOngoingNotification = async (
input: AndroidOngoingNotificationInput,
options?: UpdateAndroidOngoingNotificationOptions
): Promise => {
- return (await VoltraModule.updateAndroidOngoingNotification(
+ return (await getNativeVoltraAndroid().updateAndroidOngoingNotification(
notificationId,
serializeAndroidOngoingNotificationInput(input),
getFilteredAndroidOngoingNotificationUpdateOptions(options)
@@ -260,31 +260,33 @@ export const openAndroidNotificationSettings = async (): Promise => {
return
}
- return VoltraModule.openAndroidNotificationSettings()
+ return getNativeVoltraAndroid().openAndroidNotificationSettings()
}
export const stopAndroidOngoingNotification = async (
notificationId: string
): Promise => {
- return (await VoltraModule.stopAndroidOngoingNotification(notificationId)) as AndroidOngoingNotificationStopResult
+ return (await getNativeVoltraAndroid().stopAndroidOngoingNotification(
+ notificationId
+ )) as AndroidOngoingNotificationStopResult
}
export const isAndroidOngoingNotificationActive = (notificationId: string): boolean => {
- return VoltraModule.isAndroidOngoingNotificationActive(notificationId)
+ return getNativeVoltraAndroid().isAndroidOngoingNotificationActive(notificationId)
}
export const getAndroidOngoingNotificationStatus = (notificationId: string): AndroidOngoingNotificationStatus => {
- return VoltraModule.getAndroidOngoingNotificationStatus(notificationId)
+ return getNativeVoltraAndroid().getAndroidOngoingNotificationStatus(notificationId)
}
export async function endAllAndroidOngoingNotifications(): Promise {
- return VoltraModule.endAllAndroidOngoingNotifications()
+ return getNativeVoltraAndroid().endAllAndroidOngoingNotifications()
}
export const canPostPromotedAndroidNotifications = (): boolean => {
- return VoltraModule.canPostPromotedAndroidNotifications()
+ return getNativeVoltraAndroid().canPostPromotedAndroidNotifications()
}
export const getAndroidOngoingNotificationCapabilities = (): AndroidOngoingNotificationCapabilities => {
- return VoltraModule.getAndroidOngoingNotificationCapabilities()
+ return getNativeVoltraAndroid().getAndroidOngoingNotificationCapabilities()
}
diff --git a/packages/android-client/src/preload.ts b/packages/android-client/src/preload.ts
index bce01ce0..c3bdc071 100644
--- a/packages/android-client/src/preload.ts
+++ b/packages/android-client/src/preload.ts
@@ -1,9 +1,9 @@
import type { PreloadImageOptions, PreloadImagesResult } from './types.js'
-import VoltraModule from './VoltraModule.js'
+import { getNativeVoltraAndroid } from './native/NativeVoltraAndroid.js'
export async function preloadImages(images: PreloadImageOptions[]): Promise {
try {
- return await VoltraModule.preloadImages(images)
+ return await getNativeVoltraAndroid().preloadImages(images)
} catch (error) {
return {
succeeded: [],
@@ -17,7 +17,7 @@ export async function preloadImages(images: PreloadImageOptions[]): Promise {
try {
- await VoltraModule.clearPreloadedImages(keys ?? null)
+ await getNativeVoltraAndroid().clearPreloadedImages(keys ?? null)
} catch (error) {
console.error('Failed to clear preloaded images:', error)
}
@@ -25,7 +25,7 @@ export async function clearPreloadedImages(keys?: string[]): Promise {
export async function reloadWidgets(widgetIds?: string[]): Promise {
try {
- await VoltraModule.reloadAndroidWidgets(widgetIds ?? null)
+ await getNativeVoltraAndroid().reloadAndroidWidgets(widgetIds ?? null)
} catch (error) {
console.error('Failed to reload Android widgets:', error)
}
diff --git a/packages/android-client/src/utils/isAndroid.ts b/packages/android-client/src/utils/isAndroid.ts
new file mode 100644
index 00000000..d079e1d2
--- /dev/null
+++ b/packages/android-client/src/utils/isAndroid.ts
@@ -0,0 +1,3 @@
+import { Platform } from 'react-native'
+
+export const isAndroid = (): boolean => Platform.OS === 'android'
diff --git a/packages/android-client/src/widgets/api.ts b/packages/android-client/src/widgets/api.ts
index 5118743e..8ce67d34 100644
--- a/packages/android-client/src/widgets/api.ts
+++ b/packages/android-client/src/widgets/api.ts
@@ -5,7 +5,7 @@ import {
type WidgetInfo,
} from '@use-voltra/android'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltraAndroid } from '../native/NativeVoltraAndroid.js'
export type {
AndroidWidgetSize,
@@ -22,30 +22,30 @@ export const updateAndroidWidget = async (
): Promise => {
const payload = renderAndroidWidgetToString(variants)
- return VoltraModule.updateAndroidWidget(widgetId, payload, {
+ return getNativeVoltraAndroid().updateAndroidWidget(widgetId, payload, {
deepLinkUrl: options?.deepLinkUrl,
})
}
export const reloadAndroidWidgets = async (widgetIds?: string[]): Promise => {
- return VoltraModule.reloadAndroidWidgets(widgetIds ?? null)
+ return getNativeVoltraAndroid().reloadAndroidWidgets(widgetIds ?? null)
}
export const clearAndroidWidget = async (widgetId: string): Promise => {
- return VoltraModule.clearAndroidWidget(widgetId)
+ return getNativeVoltraAndroid().clearAndroidWidget(widgetId)
}
export const clearAllAndroidWidgets = async (): Promise => {
- return VoltraModule.clearAllAndroidWidgets()
+ return getNativeVoltraAndroid().clearAllAndroidWidgets()
}
export const requestPinAndroidWidget = async (
widgetId: string,
options?: { previewWidth?: number; previewHeight?: number }
): Promise => {
- return VoltraModule.requestPinGlanceAppWidget(widgetId, options)
+ return getNativeVoltraAndroid().requestPinGlanceAppWidget(widgetId, options)
}
export const getActiveWidgets = async (): Promise => {
- return VoltraModule.getActiveWidgets()
+ return getNativeVoltraAndroid().getActiveWidgets() as Promise
}
diff --git a/packages/android-client/src/widgets/server-credentials.ts b/packages/android-client/src/widgets/server-credentials.ts
index 07a35f25..aadbef00 100644
--- a/packages/android-client/src/widgets/server-credentials.ts
+++ b/packages/android-client/src/widgets/server-credentials.ts
@@ -1,5 +1,5 @@
import type { WidgetServerCredentials } from '../types.js'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltraAndroid } from '../native/NativeVoltraAndroid.js'
export type { WidgetServerCredentials } from '../types.js'
@@ -7,9 +7,9 @@ export async function setWidgetServerCredentials(credentials: WidgetServerCreden
if (!credentials.token) {
throw new Error('[Voltra] [Android] setWidgetServerCredentials: token is required')
}
- return VoltraModule.setWidgetServerCredentials(credentials)
+ return getNativeVoltraAndroid().setWidgetServerCredentials(credentials)
}
export async function clearWidgetServerCredentials(): Promise {
- return VoltraModule.clearWidgetServerCredentials()
+ return getNativeVoltraAndroid().clearWidgetServerCredentials()
}
diff --git a/packages/android-client/tsconfig.build.json b/packages/android-client/tsconfig.build.json
new file mode 100644
index 00000000..962cbcd4
--- /dev/null
+++ b/packages/android-client/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.base.json",
+ "include": ["./src"],
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
+}
diff --git a/packages/android-client/tsconfig.typecheck.json b/packages/android-client/tsconfig.typecheck.json
index 477960f6..7828ecfe 100644
--- a/packages/android-client/tsconfig.typecheck.json
+++ b/packages/android-client/tsconfig.typecheck.json
@@ -5,9 +5,8 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
+ "@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
"@use-voltra/android-server": ["packages/android-server/src/index.ts"],
"@use-voltra/core": ["packages/core/src/index.ts"],
diff --git a/packages/android-server/README.md b/packages/android-server/README.md
index af00eb6e..4a59d249 100644
--- a/packages/android-server/README.md
+++ b/packages/android-server/README.md
@@ -23,13 +23,13 @@ const payload = renderAndroidOngoingNotificationPayloadToJson(
Use this package only in backend or server-side environments. Do not import server renderers from React Native app runtime code.
> [!WARNING]
-> This package is not intended to be installed directly in your app. Most apps should install `voltra` instead.
+> Use in Node.js / backend only — not in React Native app bundles.
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
+Renders Android widget and ongoing-notification JSX to payloads. See [use-voltra.dev](https://use-voltra.dev/android/development/server-driven-widgets).
## 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.
@@ -38,9 +38,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/android-server?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/android-server?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/android-server
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ./CONTRIBUTING.md
diff --git a/packages/android-server/package.json b/packages/android-server/package.json
index 486070b5..e4b6e5e3 100644
--- a/packages/android-server/package.json
+++ b/packages/android-server/package.json
@@ -22,6 +22,7 @@
"build": "node ../../scripts/build-package.mjs packages/android-server",
"clean": "rm -rf build",
"lint": "oxlint src",
+ "test": "node --test",
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
},
"dependencies": {
diff --git a/packages/android-server/src/index.ts b/packages/android-server/src/index.ts
index b3c2381b..27fa1048 100644
--- a/packages/android-server/src/index.ts
+++ b/packages/android-server/src/index.ts
@@ -6,6 +6,7 @@ export {
renderAndroidOngoingNotificationPayload,
renderAndroidOngoingNotificationPayloadToJson,
} from '@use-voltra/android/server'
+import { getAndroidComponentId } from '@use-voltra/android'
import type {
WidgetRenderRequest,
WidgetUpdateExpressHandler,
@@ -54,45 +55,8 @@ export type AndroidWidgetVariants = AndroidWidgetSizeVariant[]
type AndroidWidgetRenderOptions = Record
-const ANDROID_COMPONENT_NAME_TO_ID: Record = {
- AndroidFilledButton: 0,
- AndroidImage: 1,
- AndroidSwitch: 2,
- AndroidCheckBox: 3,
- AndroidRadioButton: 4,
- AndroidBox: 5,
- AndroidButton: 6,
- AndroidCircleIconButton: 7,
- AndroidCircularProgressIndicator: 8,
- AndroidColumn: 9,
- AndroidLazyColumn: 10,
- AndroidLazyVerticalGrid: 11,
- AndroidLinearProgressIndicator: 12,
- AndroidOutlineButton: 13,
- AndroidRow: 14,
- AndroidScaffold: 15,
- AndroidSpacer: 16,
- AndroidSquareIconButton: 17,
- AndroidText: 18,
- AndroidTitleBar: 19,
- AndroidChart: 20,
-}
-
-const getAndroidComponentId = (name: string): number => {
- const id = ANDROID_COMPONENT_NAME_TO_ID[name]
- if (id === undefined) {
- throw new Error(
- `Unknown Android component name: "${name}". Available components: ${Object.keys(
- ANDROID_COMPONENT_NAME_TO_ID
- ).join(', ')}`
- )
- }
-
- return id
-}
-
const androidComponentRegistry = {
- getComponentId: (name: string) => getAndroidComponentId(name),
+ getComponentId: getAndroidComponentId,
}
export const renderAndroidWidgetToJson = (
diff --git a/packages/android-server/test/index.test.js b/packages/android-server/test/index.test.js
new file mode 100644
index 00000000..56a5594c
--- /dev/null
+++ b/packages/android-server/test/index.test.js
@@ -0,0 +1,194 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+
+const React = require('react')
+
+const {
+ createAndroidWidgetUpdateExpressHandler,
+ createAndroidWidgetUpdateHandler,
+ createAndroidWidgetUpdateNodeHandler,
+ renderAndroidWidgetToString,
+} = require('../build/cjs/index.js')
+const { VoltraAndroid } = require('../../android/build/cjs/index.js')
+
+function createNodeResponseRecorder() {
+ return {
+ status: undefined,
+ headers: undefined,
+ body: undefined,
+ writeHead(status, headers) {
+ this.status = status
+ this.headers = headers
+ },
+ end(body) {
+ this.body = body
+ },
+ }
+}
+
+function createWidgetVariants(label) {
+ return [
+ {
+ size: { width: 150, height: 80 },
+ content: React.createElement(VoltraAndroid.Box, null, React.createElement(VoltraAndroid.Text, null, label)),
+ },
+ ]
+}
+
+test('serializes Android widget variants before returning the shared response', async () => {
+ const variants = createWidgetVariants('Hello from Android')
+ const calls = []
+ const handler = createAndroidWidgetUpdateHandler({
+ render(request) {
+ calls.push(request)
+ return variants
+ },
+ })
+
+ const response = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=android&theme=dark', {
+ headers: {
+ authorization: 'Bearer android-token',
+ 'x-voltra-request': 'android-fetch',
+ },
+ })
+ )
+
+ assert.equal(response.status, 200)
+ assert.equal(await response.text(), renderAndroidWidgetToString(variants))
+ assert.equal(calls.length, 1)
+ assert.equal(calls[0].widgetId, 'weather')
+ assert.equal(calls[0].platform, 'android')
+ assert.equal(calls[0].theme, 'dark')
+ assert.equal(calls[0].family, undefined)
+ assert.equal(calls[0].token, 'android-token')
+ assert.equal(calls[0].headers.authorization, 'Bearer android-token')
+ assert.equal(calls[0].headers['x-voltra-request'], 'android-fetch')
+})
+
+test('passes validateToken through unchanged and preserves null render results', async () => {
+ const authCalls = []
+ const handler = createAndroidWidgetUpdateHandler({
+ render() {
+ return null
+ },
+ validateToken(token) {
+ authCalls.push(token)
+ return token === 'valid-token'
+ },
+ })
+
+ const missingToken = await handler(new Request('https://example.com/update?widgetId=weather&platform=android'))
+ assert.equal(missingToken.status, 401)
+ assert.deepEqual(await missingToken.json(), { error: 'Authorization required' })
+ assert.deepEqual(authCalls, [])
+
+ const invalidToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=android', {
+ headers: { authorization: 'Bearer nope' },
+ })
+ )
+ assert.equal(invalidToken.status, 401)
+ assert.deepEqual(await invalidToken.json(), { error: 'Invalid token' })
+
+ const validToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=android', {
+ headers: { authorization: 'Bearer valid-token' },
+ })
+ )
+ assert.equal(validToken.status, 404)
+ assert.deepEqual(await validToken.json(), { error: 'No content for Android widget: weather' })
+ assert.deepEqual(authCalls, ['nope', 'valid-token'])
+})
+
+test('Node and Express adapters delegate through the same conversion path', async () => {
+ const variants = createWidgetVariants('Hello from adapter')
+ const calls = []
+ const options = {
+ render(request) {
+ calls.push({
+ widgetId: request.widgetId,
+ platform: request.platform,
+ theme: request.theme,
+ family: request.family,
+ token: request.token,
+ url: request.url.toString(),
+ headers: request.headers,
+ })
+ return variants
+ },
+ }
+
+ const nodeHandler = createAndroidWidgetUpdateNodeHandler(options)
+ const nodeResponse = createNodeResponseRecorder()
+ await nodeHandler(
+ {
+ url: '/update?widgetId=weather&platform=android',
+ method: 'POST',
+ headers: {
+ host: 'widgets.example.com',
+ authorization: 'Bearer node-token',
+ 'x-voltra-request': 'node',
+ },
+ socket: { encrypted: true },
+ },
+ nodeResponse
+ )
+
+ assert.equal(nodeResponse.status, 200)
+ assert.deepEqual(nodeResponse.headers, {
+ 'cache-control': 'no-cache',
+ 'content-type': 'application/json',
+ })
+ assert.equal(nodeResponse.body, renderAndroidWidgetToString(variants))
+
+ const expressHandler = createAndroidWidgetUpdateExpressHandler(options)
+ const expressResponse = createNodeResponseRecorder()
+ await expressHandler(
+ {
+ url: '/update?widgetId=weather&platform=android&theme=dark',
+ method: 'GET',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ socket: {},
+ },
+ expressResponse
+ )
+
+ assert.equal(expressResponse.status, 200)
+ assert.deepEqual(expressResponse.headers, {
+ 'cache-control': 'no-cache',
+ 'content-type': 'application/json',
+ })
+ assert.equal(expressResponse.body, renderAndroidWidgetToString(variants))
+
+ assert.deepEqual(calls, [
+ {
+ widgetId: 'weather',
+ platform: 'android',
+ theme: 'light',
+ family: undefined,
+ token: 'node-token',
+ url: 'https://widgets.example.com/update?widgetId=weather&platform=android',
+ headers: {
+ authorization: 'Bearer node-token',
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'node',
+ },
+ },
+ {
+ widgetId: 'weather',
+ platform: 'android',
+ theme: 'dark',
+ family: undefined,
+ token: undefined,
+ url: 'http://widgets.example.com/update?widgetId=weather&platform=android&theme=dark',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ },
+ ])
+})
diff --git a/packages/android-server/tsconfig.typecheck.json b/packages/android-server/tsconfig.typecheck.json
index 0de799cd..ede85286 100644
--- a/packages/android-server/tsconfig.typecheck.json
+++ b/packages/android-server/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/android/README.md b/packages/android/README.md
index 146a4b6e..d3a58893 100644
--- a/packages/android/README.md
+++ b/packages/android/README.md
@@ -1,19 +1,20 @@

-### Voltra for Android
+### Voltra for Android — JSX and rendering
[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
-`@use-voltra/android` contains the Android implementation package for Voltra, including the JSX namespace, Android widgets, and Android live update APIs.
+`@use-voltra/android` contains the Android JSX namespace (`VoltraAndroid`), dynamic colors, payload rendering, and the `@use-voltra/android/server` entry for Node.js.
-> [!WARNING]
-> This package is not intended to be installed directly in your app. Most apps should install `voltra` instead.
+**React Native apps** should install [`@use-voltra/android-client`](../android-client) only. The client package depends on this one and re-exports everything you need for app code, including `VoltraAndroid`.
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
+Use `@use-voltra/android` directly when you need server rendering (`@use-voltra/android-server` builds on top of it) or when working inside the monorepo.
+
+See [use-voltra.dev](https://use-voltra.dev/android/setup) for app setup.
## 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.
@@ -22,9 +23,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/android?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/android?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/android
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
-[prs-welcome]: ./CONTRIBUTING.md
+[prs-welcome]: ../../CONTRIBUTING.md
diff --git a/packages/android/package.json b/packages/android/package.json
index e31fe564..09483830 100644
--- a/packages/android/package.json
+++ b/packages/android/package.json
@@ -34,7 +34,8 @@
"build": "node ../../scripts/build-package.mjs packages/android",
"clean": "rm -rf build",
"lint": "oxlint src",
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
+ "test": "node --test"
},
"dependencies": {
"@use-voltra/core": "1.4.1"
diff --git a/packages/android/src/index.ts b/packages/android/src/index.ts
index 2ce1363c..f65b1efa 100644
--- a/packages/android/src/index.ts
+++ b/packages/android/src/index.ts
@@ -1,6 +1,12 @@
// Android component namespace
export * as VoltraAndroid from './jsx/primitives.js'
export { AndroidDynamicColors } from './dynamic-colors.js'
+export {
+ getAndroidComponentId,
+ getAndroidComponentName,
+ ANDROID_COMPONENT_ID_TO_NAME,
+ ANDROID_COMPONENT_NAME_TO_ID,
+} from './payload/component-ids.js'
export { renderAndroidLiveUpdateToJson, renderAndroidLiveUpdateToString } from './live-update/renderer.js'
export { AndroidOngoingNotification } from './ongoing-notification/components.js'
export { renderAndroidOngoingNotificationPayload } from './ongoing-notification/renderer.js'
diff --git a/packages/android/src/jsx/Box.tsx b/packages/android/src/jsx/Box.tsx
index 423b82d9..9d078cb2 100644
--- a/packages/android/src/jsx/Box.tsx
+++ b/packages/android/src/jsx/Box.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { BoxProps } from './props/Box.js'
+import type { AndroidBoxProps as BoxProps } from './props/AndroidBox.js'
export type { BoxProps }
export const Box = createVoltraComponent('AndroidBox')
diff --git a/packages/android/src/jsx/Button.tsx b/packages/android/src/jsx/Button.tsx
index f6f27844..06bdfe65 100644
--- a/packages/android/src/jsx/Button.tsx
+++ b/packages/android/src/jsx/Button.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { ButtonProps } from './props/Button.js'
+import type { AndroidButtonProps as ButtonProps } from './props/AndroidButton.js'
export type { ButtonProps }
export const Button = createVoltraComponent('AndroidButton')
diff --git a/packages/android/src/jsx/Chart.tsx b/packages/android/src/jsx/Chart.tsx
index a89af573..f4c157b4 100644
--- a/packages/android/src/jsx/Chart.tsx
+++ b/packages/android/src/jsx/Chart.tsx
@@ -1,6 +1,6 @@
import React from 'react'
-import type { VoltraAndroidBaseProps } from '../jsx/baseProps.js'
+import type { VoltraAndroidBaseProps } from './baseProps.js'
import type { AreaMarkProps } from './AreaMark.js'
import type { BarMarkProps } from './BarMark.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
diff --git a/packages/android/src/jsx/CheckBox.tsx b/packages/android/src/jsx/CheckBox.tsx
index 076bf5f0..597041fe 100644
--- a/packages/android/src/jsx/CheckBox.tsx
+++ b/packages/android/src/jsx/CheckBox.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { CheckBoxProps } from './props/CheckBox.js'
+import type { AndroidCheckBoxProps as CheckBoxProps } from './props/AndroidCheckBox.js'
export type { CheckBoxProps }
export const CheckBox = createVoltraComponent('AndroidCheckBox')
diff --git a/packages/android/src/jsx/CircleIconButton.tsx b/packages/android/src/jsx/CircleIconButton.tsx
index e3e4d97c..249efdfb 100644
--- a/packages/android/src/jsx/CircleIconButton.tsx
+++ b/packages/android/src/jsx/CircleIconButton.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { CircleIconButtonProps } from './props/CircleIconButton.js'
+import type { AndroidCircleIconButtonProps as CircleIconButtonProps } from './props/AndroidCircleIconButton.js'
export type { CircleIconButtonProps }
export const CircleIconButton = createVoltraComponent('AndroidCircleIconButton', {
diff --git a/packages/android/src/jsx/CircularProgressIndicator.tsx b/packages/android/src/jsx/CircularProgressIndicator.tsx
index 042a1177..386a3bc6 100644
--- a/packages/android/src/jsx/CircularProgressIndicator.tsx
+++ b/packages/android/src/jsx/CircularProgressIndicator.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { CircularProgressIndicatorProps } from './props/CircularProgressIndicator.js'
+import type { AndroidCircularProgressIndicatorProps as CircularProgressIndicatorProps } from './props/AndroidCircularProgressIndicator.js'
export type { CircularProgressIndicatorProps }
export const CircularProgressIndicator = createVoltraComponent(
diff --git a/packages/android/src/jsx/Column.tsx b/packages/android/src/jsx/Column.tsx
index fcd47d56..e4b23495 100644
--- a/packages/android/src/jsx/Column.tsx
+++ b/packages/android/src/jsx/Column.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { ColumnProps } from './props/Column.js'
+import type { AndroidColumnProps as ColumnProps } from './props/AndroidColumn.js'
export type { ColumnProps }
export const Column = createVoltraComponent('AndroidColumn')
diff --git a/packages/android/src/jsx/FilledButton.tsx b/packages/android/src/jsx/FilledButton.tsx
index 7646ea09..51326c36 100644
--- a/packages/android/src/jsx/FilledButton.tsx
+++ b/packages/android/src/jsx/FilledButton.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { FilledButtonProps } from './props/FilledButton.js'
+import type { AndroidFilledButtonProps as FilledButtonProps } from './props/AndroidFilledButton.js'
export type { FilledButtonProps }
export const FilledButton = createVoltraComponent('AndroidFilledButton', {
diff --git a/packages/android/src/jsx/Image.tsx b/packages/android/src/jsx/Image.tsx
index 722298e7..a8d7dbf5 100644
--- a/packages/android/src/jsx/Image.tsx
+++ b/packages/android/src/jsx/Image.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { ImageProps } from './props/Image.js'
+import type { AndroidImageProps as ImageProps } from './props/AndroidImage.js'
export type { ImageProps }
export type ImageSource = { assetName: string } | { base64: string }
diff --git a/packages/android/src/jsx/LazyColumn.tsx b/packages/android/src/jsx/LazyColumn.tsx
index da78f00a..7251fb31 100644
--- a/packages/android/src/jsx/LazyColumn.tsx
+++ b/packages/android/src/jsx/LazyColumn.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { LazyColumnProps } from './props/LazyColumn.js'
+import type { AndroidLazyColumnProps as LazyColumnProps } from './props/AndroidLazyColumn.js'
export type { LazyColumnProps }
export const LazyColumn = createVoltraComponent('AndroidLazyColumn')
diff --git a/packages/android/src/jsx/LazyVerticalGrid.tsx b/packages/android/src/jsx/LazyVerticalGrid.tsx
index 5cf5dfc0..92ca9709 100644
--- a/packages/android/src/jsx/LazyVerticalGrid.tsx
+++ b/packages/android/src/jsx/LazyVerticalGrid.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { LazyVerticalGridProps } from './props/LazyVerticalGrid.js'
+import type { AndroidLazyVerticalGridProps as LazyVerticalGridProps } from './props/AndroidLazyVerticalGrid.js'
export type { LazyVerticalGridProps }
export const LazyVerticalGrid = createVoltraComponent('AndroidLazyVerticalGrid')
diff --git a/packages/android/src/jsx/LinearProgressIndicator.tsx b/packages/android/src/jsx/LinearProgressIndicator.tsx
index 79dd6c1e..0e1933dc 100644
--- a/packages/android/src/jsx/LinearProgressIndicator.tsx
+++ b/packages/android/src/jsx/LinearProgressIndicator.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { LinearProgressIndicatorProps } from './props/LinearProgressIndicator.js'
+import type { AndroidLinearProgressIndicatorProps as LinearProgressIndicatorProps } from './props/AndroidLinearProgressIndicator.js'
export type { LinearProgressIndicatorProps }
export const LinearProgressIndicator = createVoltraComponent(
diff --git a/packages/android/src/jsx/OutlineButton.tsx b/packages/android/src/jsx/OutlineButton.tsx
index a36b2514..74561506 100644
--- a/packages/android/src/jsx/OutlineButton.tsx
+++ b/packages/android/src/jsx/OutlineButton.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { OutlineButtonProps } from './props/OutlineButton.js'
+import type { AndroidOutlineButtonProps as OutlineButtonProps } from './props/AndroidOutlineButton.js'
export type { OutlineButtonProps }
export const OutlineButton = createVoltraComponent('AndroidOutlineButton', {
diff --git a/packages/android/src/jsx/RadioButton.tsx b/packages/android/src/jsx/RadioButton.tsx
index 2ad6d9e5..28d02213 100644
--- a/packages/android/src/jsx/RadioButton.tsx
+++ b/packages/android/src/jsx/RadioButton.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { RadioButtonProps } from './props/RadioButton.js'
+import type { AndroidRadioButtonProps as RadioButtonProps } from './props/AndroidRadioButton.js'
export type { RadioButtonProps }
export const RadioButton = createVoltraComponent('AndroidRadioButton')
diff --git a/packages/android/src/jsx/Row.tsx b/packages/android/src/jsx/Row.tsx
index b72baf57..2398ed5f 100644
--- a/packages/android/src/jsx/Row.tsx
+++ b/packages/android/src/jsx/Row.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { RowProps } from './props/Row.js'
+import type { AndroidRowProps as RowProps } from './props/AndroidRow.js'
export type { RowProps }
export const Row = createVoltraComponent('AndroidRow')
diff --git a/packages/android/src/jsx/Scaffold.tsx b/packages/android/src/jsx/Scaffold.tsx
index 535f39f1..7d05ad1f 100644
--- a/packages/android/src/jsx/Scaffold.tsx
+++ b/packages/android/src/jsx/Scaffold.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { ScaffoldProps } from './props/Scaffold.js'
+import type { AndroidScaffoldProps as ScaffoldProps } from './props/AndroidScaffold.js'
export type { ScaffoldProps }
export const Scaffold = createVoltraComponent('AndroidScaffold')
diff --git a/packages/android/src/jsx/Spacer.tsx b/packages/android/src/jsx/Spacer.tsx
index 836793b9..03541648 100644
--- a/packages/android/src/jsx/Spacer.tsx
+++ b/packages/android/src/jsx/Spacer.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { SpacerProps } from './props/Spacer.js'
+import type { AndroidSpacerProps as SpacerProps } from './props/AndroidSpacer.js'
export type { SpacerProps }
export const Spacer = createVoltraComponent('AndroidSpacer')
diff --git a/packages/android/src/jsx/SquareIconButton.tsx b/packages/android/src/jsx/SquareIconButton.tsx
index 9f3c972e..fa3371dc 100644
--- a/packages/android/src/jsx/SquareIconButton.tsx
+++ b/packages/android/src/jsx/SquareIconButton.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { SquareIconButtonProps } from './props/SquareIconButton.js'
+import type { AndroidSquareIconButtonProps as SquareIconButtonProps } from './props/AndroidSquareIconButton.js'
export type { SquareIconButtonProps }
export const SquareIconButton = createVoltraComponent('AndroidSquareIconButton', {
diff --git a/packages/android/src/jsx/Switch.tsx b/packages/android/src/jsx/Switch.tsx
index 38a27d63..7b7dbb51 100644
--- a/packages/android/src/jsx/Switch.tsx
+++ b/packages/android/src/jsx/Switch.tsx
@@ -1,5 +1,5 @@
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { SwitchProps } from './props/Switch.js'
+import type { AndroidSwitchProps as SwitchProps } from './props/AndroidSwitch.js'
export type { SwitchProps }
export const Switch = createVoltraComponent('AndroidSwitch')
diff --git a/packages/android/src/jsx/Text.tsx b/packages/android/src/jsx/Text.tsx
index 5b74ba44..04b9b15a 100644
--- a/packages/android/src/jsx/Text.tsx
+++ b/packages/android/src/jsx/Text.tsx
@@ -1,9 +1,9 @@
import { VoltraAndroidTextStyleProp } from '../styles/types.js'
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { TextProps as GeneratedTextProps } from './props/Text.js'
+import type { AndroidTextProps } from './props/AndroidText.js'
// Update 'style' to use Android text style prop
-export type TextProps = Omit & {
+export type TextProps = Omit & {
style?: VoltraAndroidTextStyleProp
}
diff --git a/packages/android/src/jsx/TitleBar.tsx b/packages/android/src/jsx/TitleBar.tsx
index 854d7643..72bee796 100644
--- a/packages/android/src/jsx/TitleBar.tsx
+++ b/packages/android/src/jsx/TitleBar.tsx
@@ -1,7 +1,11 @@
+import type { ImageSource } from './Image.js'
import { createVoltraComponent } from './createVoltraComponent.js'
-import type { TitleBarProps } from './props/TitleBar.js'
+import type { AndroidTitleBarProps } from './props/AndroidTitleBar.js'
-export type { TitleBarProps }
+/** Wire payload supports an optional leading icon (not yet in generated AndroidTitleBar props). */
+export type TitleBarProps = AndroidTitleBarProps & {
+ startIcon?: ImageSource
+}
export const TitleBar = createVoltraComponent('AndroidTitleBar', {
toJSON: (props) => {
const { startIcon, ...rest } = props
diff --git a/packages/android/src/jsx/baseProps.tsx b/packages/android/src/jsx/baseProps.tsx
index 429c154b..5b5c2445 100644
--- a/packages/android/src/jsx/baseProps.tsx
+++ b/packages/android/src/jsx/baseProps.tsx
@@ -8,3 +8,6 @@ export type VoltraAndroidBaseProps = {
style?: VoltraAndroidStyleProp
children?: ReactNode
}
+
+/** Alias used by generated `props/*.ts` component types */
+export type VoltraBaseProps = VoltraAndroidBaseProps
diff --git a/packages/android/src/jsx/createVoltraComponent.ts b/packages/android/src/jsx/createVoltraComponent.ts
index 2b166b01..d3ff02ae 100644
--- a/packages/android/src/jsx/createVoltraComponent.ts
+++ b/packages/android/src/jsx/createVoltraComponent.ts
@@ -1,36 +1,2 @@
-import type { ComponentType } from 'react'
-import { createElement } from 'react'
-
-export const VOLTRA_COMPONENT_TAG = Symbol.for('VOLTRA_COMPONENT_TAG')
-
-export type VoltraComponent> = ComponentType & {
- displayName: string
- [VOLTRA_COMPONENT_TAG]: true
-}
-
-export type VoltraComponentOptions> = {
- toJSON?: (props: TProps) => Record
-}
-
-export const createVoltraComponent = >(
- componentName: string,
- options?: VoltraComponentOptions
-): VoltraComponent => {
- const Component = (props: TProps) => {
- const toJSON = options?.toJSON ? options.toJSON : (value: TProps) => value
- const normalizedProps = toJSON(props)
-
- return createElement(componentName, normalizedProps)
- }
-
- Component[VOLTRA_COMPONENT_TAG] = true
- Component.displayName = componentName
-
- return Component as VoltraComponent
-}
-
-export const isVoltraComponent = >(
- component: ComponentType
-): component is VoltraComponent => {
- return typeof component === 'function' && VOLTRA_COMPONENT_TAG in component
-}
+export { createVoltraComponent, isVoltraComponent, VOLTRA_COMPONENT_TAG } from '@use-voltra/core'
+export type { VoltraComponent, VoltraComponentOptions } from '@use-voltra/core'
diff --git a/packages/voltra/src/jsx/props/.generated b/packages/android/src/jsx/props/.generated
similarity index 84%
rename from packages/voltra/src/jsx/props/.generated
rename to packages/android/src/jsx/props/.generated
index 5f614e22..d35c2e9e 100644
--- a/packages/voltra/src/jsx/props/.generated
+++ b/packages/android/src/jsx/props/.generated
@@ -1,4 +1,4 @@
-This directory contains auto-generated component props type files.
+This directory contains auto-generated Android component props type files.
DO NOT EDIT MANUALLY.
Generated from: data/components.json
diff --git a/packages/ios/src/jsx/props/AndroidBox.ts b/packages/android/src/jsx/props/AndroidBox.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidBox.ts
rename to packages/android/src/jsx/props/AndroidBox.ts
diff --git a/packages/ios/src/jsx/props/AndroidButton.ts b/packages/android/src/jsx/props/AndroidButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidButton.ts
rename to packages/android/src/jsx/props/AndroidButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidChart.ts b/packages/android/src/jsx/props/AndroidChart.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidChart.ts
rename to packages/android/src/jsx/props/AndroidChart.ts
diff --git a/packages/ios/src/jsx/props/AndroidCheckBox.ts b/packages/android/src/jsx/props/AndroidCheckBox.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidCheckBox.ts
rename to packages/android/src/jsx/props/AndroidCheckBox.ts
diff --git a/packages/ios/src/jsx/props/AndroidCircleIconButton.ts b/packages/android/src/jsx/props/AndroidCircleIconButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidCircleIconButton.ts
rename to packages/android/src/jsx/props/AndroidCircleIconButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidCircularProgressIndicator.ts b/packages/android/src/jsx/props/AndroidCircularProgressIndicator.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidCircularProgressIndicator.ts
rename to packages/android/src/jsx/props/AndroidCircularProgressIndicator.ts
diff --git a/packages/ios/src/jsx/props/AndroidColumn.ts b/packages/android/src/jsx/props/AndroidColumn.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidColumn.ts
rename to packages/android/src/jsx/props/AndroidColumn.ts
diff --git a/packages/ios/src/jsx/props/AndroidFilledButton.ts b/packages/android/src/jsx/props/AndroidFilledButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidFilledButton.ts
rename to packages/android/src/jsx/props/AndroidFilledButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidImage.ts b/packages/android/src/jsx/props/AndroidImage.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidImage.ts
rename to packages/android/src/jsx/props/AndroidImage.ts
diff --git a/packages/ios/src/jsx/props/AndroidLazyColumn.ts b/packages/android/src/jsx/props/AndroidLazyColumn.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidLazyColumn.ts
rename to packages/android/src/jsx/props/AndroidLazyColumn.ts
diff --git a/packages/ios/src/jsx/props/AndroidLazyVerticalGrid.ts b/packages/android/src/jsx/props/AndroidLazyVerticalGrid.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidLazyVerticalGrid.ts
rename to packages/android/src/jsx/props/AndroidLazyVerticalGrid.ts
diff --git a/packages/ios/src/jsx/props/AndroidLinearProgressIndicator.ts b/packages/android/src/jsx/props/AndroidLinearProgressIndicator.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidLinearProgressIndicator.ts
rename to packages/android/src/jsx/props/AndroidLinearProgressIndicator.ts
diff --git a/packages/ios/src/jsx/props/AndroidOutlineButton.ts b/packages/android/src/jsx/props/AndroidOutlineButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidOutlineButton.ts
rename to packages/android/src/jsx/props/AndroidOutlineButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidRadioButton.ts b/packages/android/src/jsx/props/AndroidRadioButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidRadioButton.ts
rename to packages/android/src/jsx/props/AndroidRadioButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidRow.ts b/packages/android/src/jsx/props/AndroidRow.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidRow.ts
rename to packages/android/src/jsx/props/AndroidRow.ts
diff --git a/packages/ios/src/jsx/props/AndroidScaffold.ts b/packages/android/src/jsx/props/AndroidScaffold.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidScaffold.ts
rename to packages/android/src/jsx/props/AndroidScaffold.ts
diff --git a/packages/ios/src/jsx/props/AndroidSpacer.ts b/packages/android/src/jsx/props/AndroidSpacer.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidSpacer.ts
rename to packages/android/src/jsx/props/AndroidSpacer.ts
diff --git a/packages/ios/src/jsx/props/AndroidSquareIconButton.ts b/packages/android/src/jsx/props/AndroidSquareIconButton.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidSquareIconButton.ts
rename to packages/android/src/jsx/props/AndroidSquareIconButton.ts
diff --git a/packages/ios/src/jsx/props/AndroidSwitch.ts b/packages/android/src/jsx/props/AndroidSwitch.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidSwitch.ts
rename to packages/android/src/jsx/props/AndroidSwitch.ts
diff --git a/packages/ios/src/jsx/props/AndroidText.ts b/packages/android/src/jsx/props/AndroidText.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidText.ts
rename to packages/android/src/jsx/props/AndroidText.ts
diff --git a/packages/ios/src/jsx/props/AndroidTitleBar.ts b/packages/android/src/jsx/props/AndroidTitleBar.ts
similarity index 100%
rename from packages/ios/src/jsx/props/AndroidTitleBar.ts
rename to packages/android/src/jsx/props/AndroidTitleBar.ts
diff --git a/packages/android/src/jsx/props/Box.ts b/packages/android/src/jsx/props/Box.ts
deleted file mode 100644
index 900eb6e4..00000000
--- a/packages/android/src/jsx/props/Box.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type BoxProps = VoltraAndroidBaseProps & {
- /** Content alignment within the box */
- contentAlignment?:
- | 'top-start'
- | 'top-center'
- | 'top-end'
- | 'center-start'
- | 'center'
- | 'center-end'
- | 'bottom-start'
- | 'bottom-center'
- | 'bottom-end'
-}
diff --git a/packages/android/src/jsx/props/Button.ts b/packages/android/src/jsx/props/Button.ts
deleted file mode 100644
index 0bb2c1d1..00000000
--- a/packages/android/src/jsx/props/Button.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type ButtonProps = VoltraAndroidBaseProps & {
- /** Whether the button is enabled */
- enabled?: boolean
-}
diff --git a/packages/android/src/jsx/props/CheckBox.ts b/packages/android/src/jsx/props/CheckBox.ts
deleted file mode 100644
index 3684a057..00000000
--- a/packages/android/src/jsx/props/CheckBox.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type CheckBoxProps = VoltraAndroidBaseProps & {
- /** Whether the checkbox is checked */
- checked?: boolean
- /** Text to display next to the checkbox */
- text?: string
- /** Style for the text */
- style?: VoltraAndroidTextStyleProp
- /** Color when checked */
- checkedColor?: AndroidColorValue
- /** Color when unchecked */
- uncheckedColor?: AndroidColorValue
- /** Maximum lines for text */
- maxLines?: number
-}
diff --git a/packages/android/src/jsx/props/CircleIconButton.ts b/packages/android/src/jsx/props/CircleIconButton.ts
deleted file mode 100644
index bf215685..00000000
--- a/packages/android/src/jsx/props/CircleIconButton.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-import type { ImageSource } from '../Image.js'
-
-export type CircleIconButtonProps = VoltraAndroidBaseProps & {
- /** Whether the button is enabled */
- enabled?: boolean
- /** Icon source */
- icon?: ImageSource
- /** Content description for accessibility */
- contentDescription?: string
- /** Background color */
- backgroundColor?: AndroidColorValue
- /** Icon color */
- contentColor?: AndroidColorValue
-}
diff --git a/packages/android/src/jsx/props/CircularProgressIndicator.ts b/packages/android/src/jsx/props/CircularProgressIndicator.ts
deleted file mode 100644
index acaa7ef9..00000000
--- a/packages/android/src/jsx/props/CircularProgressIndicator.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type CircularProgressIndicatorProps = VoltraAndroidBaseProps & {
- /**
- * Current progress value (0.0 to 1.0)
- * @note Android Glance only supports indeterminate mode - this prop is ignored on Android.
- * Use LinearProgressIndicator if you need determinate progress on Android.
- */
- progress?: number
- /** Color for the progress indicator */
- color?: AndroidColorValue
-}
diff --git a/packages/android/src/jsx/props/Column.ts b/packages/android/src/jsx/props/Column.ts
deleted file mode 100644
index f8ae2c84..00000000
--- a/packages/android/src/jsx/props/Column.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type ColumnProps = VoltraAndroidBaseProps & {
- /** Horizontal alignment of children */
- horizontalAlignment?: 'start' | 'center-horizontally' | 'end'
- /** Vertical alignment of children */
- verticalAlignment?: 'top' | 'center-vertically' | 'bottom'
-}
diff --git a/packages/android/src/jsx/props/FilledButton.ts b/packages/android/src/jsx/props/FilledButton.ts
deleted file mode 100644
index bc9adbe2..00000000
--- a/packages/android/src/jsx/props/FilledButton.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-import type { ImageSource } from '../Image.js'
-
-export type FilledButtonProps = VoltraAndroidBaseProps & {
- /** Text to display on the button */
- text: string
- /** Whether the button is enabled */
- enabled?: boolean
- /** Icon to display */
- icon?: ImageSource
- /** Background color */
- backgroundColor?: AndroidColorValue
- /** Content (text/icon) color */
- contentColor?: AndroidColorValue
- /** Maximum lines for text */
- maxLines?: number
-}
diff --git a/packages/android/src/jsx/props/Image.ts b/packages/android/src/jsx/props/Image.ts
deleted file mode 100644
index da53231c..00000000
--- a/packages/android/src/jsx/props/Image.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { ReactNode } from 'react'
-
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type ImageProps = VoltraAndroidBaseProps & {
- /** Image source - { assetName: string } for Android drawable resources or preloaded assets */
- source: Record
- /** How the image should be resized to fit its container */
- resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center'
- /** Content scale mode (Glance terminology) */
- contentScale?: 'crop' | 'fit' | 'fill-bounds'
- /** Content description for accessibility */
- contentDescription?: string
- /** Opacity (0.0 to 1.0) */
- alpha?: number
- /** Tint color */
- tintColor?: AndroidColorValue
- /** Background color used when the image is missing */
- fallbackColor?: AndroidColorValue
- /** Custom fallback content rendered when the image is missing */
- fallback?: ReactNode
-}
diff --git a/packages/android/src/jsx/props/LazyColumn.ts b/packages/android/src/jsx/props/LazyColumn.ts
deleted file mode 100644
index 05d805a2..00000000
--- a/packages/android/src/jsx/props/LazyColumn.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type LazyColumnProps = VoltraAndroidBaseProps & {
- /** Horizontal alignment of children */
- horizontalAlignment?: 'start' | 'center-horizontally' | 'end'
-}
diff --git a/packages/android/src/jsx/props/LazyVerticalGrid.ts b/packages/android/src/jsx/props/LazyVerticalGrid.ts
deleted file mode 100644
index f7c69dc3..00000000
--- a/packages/android/src/jsx/props/LazyVerticalGrid.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type LazyVerticalGridProps = VoltraAndroidBaseProps & {
- /** Number of columns or 'adaptive' for adaptive grid */
- columns: number | 'adaptive'
- /** Minimum size (in dp) for adaptive grid mode */
- minSize?: number
- /** Horizontal alignment of children */
- horizontalAlignment?: 'start' | 'center-horizontally' | 'end'
- /** Vertical alignment of children */
- verticalAlignment?: 'top' | 'center' | 'bottom'
-}
diff --git a/packages/android/src/jsx/props/LinearProgressIndicator.ts b/packages/android/src/jsx/props/LinearProgressIndicator.ts
deleted file mode 100644
index e1af142d..00000000
--- a/packages/android/src/jsx/props/LinearProgressIndicator.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type LinearProgressIndicatorProps = VoltraAndroidBaseProps & {
- /** Current progress value (0.0 to 1.0) */
- progress?: number
- /** Color for the progress indicator */
- color?: AndroidColorValue
- /** Color for the background track */
- backgroundColor?: AndroidColorValue
-}
diff --git a/packages/android/src/jsx/props/OutlineButton.ts b/packages/android/src/jsx/props/OutlineButton.ts
deleted file mode 100644
index 45cff252..00000000
--- a/packages/android/src/jsx/props/OutlineButton.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-import type { ImageSource } from '../Image.js'
-
-export type OutlineButtonProps = VoltraAndroidBaseProps & {
- /** Text to display on the button */
- text: string
- /** Whether the button is enabled */
- enabled?: boolean
- /** Icon to display */
- icon?: ImageSource
- /** Content (text/icon) color */
- contentColor?: AndroidColorValue
- /** Maximum lines for text */
- maxLines?: number
-}
diff --git a/packages/android/src/jsx/props/RadioButton.ts b/packages/android/src/jsx/props/RadioButton.ts
deleted file mode 100644
index 81a4b443..00000000
--- a/packages/android/src/jsx/props/RadioButton.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type RadioButtonProps = VoltraAndroidBaseProps & {
- /** Whether the radio button is checked */
- checked?: boolean
- /** Text to display next to the radio button */
- text?: string
- /** Style for the text */
- style?: VoltraAndroidTextStyleProp
- /** Color when checked */
- checkedColor?: AndroidColorValue
- /** Color when unchecked */
- uncheckedColor?: AndroidColorValue
- /** Maximum lines for text */
- maxLines?: number
- /** Whether the radio button is enabled */
- enabled?: boolean
-}
diff --git a/packages/android/src/jsx/props/Row.ts b/packages/android/src/jsx/props/Row.ts
deleted file mode 100644
index ca35819f..00000000
--- a/packages/android/src/jsx/props/Row.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type RowProps = VoltraAndroidBaseProps & {
- /** Vertical alignment of children */
- verticalAlignment?: 'top' | 'center-vertically' | 'bottom'
- /** Horizontal alignment of children */
- horizontalAlignment?: 'start' | 'center-horizontally' | 'end'
-}
diff --git a/packages/android/src/jsx/props/Scaffold.ts b/packages/android/src/jsx/props/Scaffold.ts
deleted file mode 100644
index ecb73631..00000000
--- a/packages/android/src/jsx/props/Scaffold.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type ScaffoldProps = VoltraAndroidBaseProps & {
- /** Background color for the scaffold - supports hex, rgb, hsl, and named colors */
- backgroundColor?: AndroidColorValue
- /** Horizontal padding */
- horizontalPadding?: number
-}
diff --git a/packages/android/src/jsx/props/Spacer.ts b/packages/android/src/jsx/props/Spacer.ts
deleted file mode 100644
index eba0d55f..00000000
--- a/packages/android/src/jsx/props/Spacer.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type SpacerProps = VoltraAndroidBaseProps & {
- /** Size of the spacer in dp */
- size?: number
-}
diff --git a/packages/android/src/jsx/props/SquareIconButton.ts b/packages/android/src/jsx/props/SquareIconButton.ts
deleted file mode 100644
index 3ee3ac4e..00000000
--- a/packages/android/src/jsx/props/SquareIconButton.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-import type { ImageSource } from '../Image.js'
-
-export type SquareIconButtonProps = VoltraAndroidBaseProps & {
- /** Whether the button is enabled */
- enabled?: boolean
- /** Icon source */
- icon?: ImageSource
- /** Content description for accessibility */
- contentDescription?: string
- /** Background color */
- backgroundColor?: AndroidColorValue
- /** Icon color */
- contentColor?: AndroidColorValue
-}
diff --git a/packages/android/src/jsx/props/Switch.ts b/packages/android/src/jsx/props/Switch.ts
deleted file mode 100644
index 807bb464..00000000
--- a/packages/android/src/jsx/props/Switch.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type SwitchProps = VoltraAndroidBaseProps & {
- /** Whether the switch is checked */
- checked?: boolean
- /** Text to display next to the switch */
- text?: string
- /** Style for the text */
- style?: VoltraAndroidTextStyleProp
- /** Thumb color when checked */
- thumbCheckedColor?: AndroidColorValue
- /** Thumb color when unchecked */
- thumbUncheckedColor?: AndroidColorValue
- /** Track color when checked */
- trackCheckedColor?: AndroidColorValue
- /** Track color when unchecked */
- trackUncheckedColor?: AndroidColorValue
- /** Maximum lines for text */
- maxLines?: number
-}
diff --git a/packages/android/src/jsx/props/Text.ts b/packages/android/src/jsx/props/Text.ts
deleted file mode 100644
index a3f7c39c..00000000
--- a/packages/android/src/jsx/props/Text.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-
-export type TextProps = VoltraAndroidBaseProps & {
- /** Maximum number of lines to display */
- maxLines?: number
- /**
- * When true, renders text as a bitmap image instead of using Glance's native Text.
- * This enables custom font support via `fontFamily` in the style prop.
- * The font file should be placed in `android/app/src/main/assets/fonts/.ttf`.
- */
- renderAsBitmap?: boolean
-}
diff --git a/packages/android/src/jsx/props/TitleBar.ts b/packages/android/src/jsx/props/TitleBar.ts
deleted file mode 100644
index 42e0ced1..00000000
--- a/packages/android/src/jsx/props/TitleBar.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { AndroidColorValue } from '../../dynamic-colors.js'
-import type { VoltraAndroidBaseProps } from '../baseProps.js'
-import type { ImageSource } from '../Image.js'
-
-export type TitleBarProps = VoltraAndroidBaseProps & {
- /** Title text to display */
- title: string
- /** Start icon source */
- startIcon: ImageSource
- /** Text color - supports hex, rgb, hsl, and named colors */
- textColor?: AndroidColorValue
- /** Icon color - supports hex, rgb, hsl, and named colors */
- iconColor?: AndroidColorValue
- /** Font family for the title */
- fontFamily?: string
-}
diff --git a/packages/android/test/renderer.test.js b/packages/android/test/renderer.test.js
new file mode 100644
index 00000000..74e17c97
--- /dev/null
+++ b/packages/android/test/renderer.test.js
@@ -0,0 +1,156 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const React = require('react')
+
+const android = require('../build/cjs/index.js')
+const { createVoltraComponent } = require('../build/cjs/jsx/createVoltraComponent.js')
+
+const {
+ ANDROID_COMPONENT_NAME_TO_ID,
+ VoltraAndroid,
+ getAndroidComponentId,
+ renderAndroidLiveUpdateToJson,
+ renderAndroidLiveUpdateToString,
+ renderAndroidViewToJson,
+ renderAndroidWidgetToJson,
+ renderAndroidWidgetToString,
+} = android
+
+test('renders Android widget variants under the expected size keys', () => {
+ const variants = [
+ {
+ size: { width: 150, height: 100 },
+ content: React.createElement(VoltraAndroid.Text, null, 'Small'),
+ },
+ {
+ size: { width: 215, height: 100 },
+ content: React.createElement(
+ VoltraAndroid.Box,
+ { testID: 'wide-root' },
+ React.createElement(VoltraAndroid.Text, null, 'Wide')
+ ),
+ },
+ {
+ size: { width: 150, height: 200 },
+ content: null,
+ },
+ ]
+
+ assert.deepEqual(renderAndroidWidgetToJson(variants), {
+ v: 1,
+ variants: {
+ '150x100': {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'Small',
+ },
+ '215x100': {
+ t: getAndroidComponentId('AndroidBox'),
+ c: {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'Wide',
+ },
+ p: {
+ testID: 'wide-root',
+ },
+ },
+ '150x200': [],
+ },
+ })
+})
+
+test('keeps Android widget string output aligned with JSON output', () => {
+ const variants = [
+ {
+ size: { width: 150, height: 100 },
+ content: React.createElement(VoltraAndroid.Text, null, 'Small'),
+ },
+ ]
+
+ assert.equal(renderAndroidWidgetToString(variants), JSON.stringify(renderAndroidWidgetToJson(variants)))
+})
+
+test('renders Android view payloads with metadata separate from variants', () => {
+ assert.deepEqual(
+ renderAndroidViewToJson(
+ React.createElement(VoltraAndroid.Column, null, React.createElement(VoltraAndroid.Text, null, 'View content'))
+ ),
+ {
+ v: 1,
+ variants: {
+ content: {
+ t: getAndroidComponentId('AndroidColumn'),
+ c: {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'View content',
+ },
+ },
+ },
+ }
+ )
+})
+
+test('renders Android live update roots and metadata into the expected fields', () => {
+ const liveUpdate = {
+ collapsed: React.createElement(VoltraAndroid.Text, null, 'Collapsed'),
+ expanded: React.createElement(VoltraAndroid.Box, null, React.createElement(VoltraAndroid.Text, null, 'Expanded')),
+ smallIcon: 'icon.png',
+ channelId: 'updates',
+ }
+
+ assert.deepEqual(renderAndroidLiveUpdateToJson(liveUpdate), {
+ v: 1,
+ collapsed: {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'Collapsed',
+ },
+ expanded: {
+ t: getAndroidComponentId('AndroidBox'),
+ c: {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'Expanded',
+ },
+ },
+ smallIcon: 'icon.png',
+ channelId: 'updates',
+ })
+
+ assert.equal(renderAndroidLiveUpdateToString(liveUpdate), JSON.stringify(renderAndroidLiveUpdateToJson(liveUpdate)))
+ assert.deepEqual(renderAndroidLiveUpdateToJson({}), { v: 1 })
+})
+
+test('uses generated Android component ids consistently in rendered payloads', () => {
+ assert.equal(ANDROID_COMPONENT_NAME_TO_ID.AndroidText, getAndroidComponentId('AndroidText'))
+ assert.equal(ANDROID_COMPONENT_NAME_TO_ID.AndroidBox, getAndroidComponentId('AndroidBox'))
+
+ const widgetJson = renderAndroidWidgetToJson([
+ {
+ size: { width: 100, height: 100 },
+ content: React.createElement(VoltraAndroid.Box, null, React.createElement(VoltraAndroid.Text, null, 'Hello')),
+ },
+ ])
+
+ assert.deepEqual(widgetJson.variants['100x100'], {
+ t: getAndroidComponentId('AndroidBox'),
+ c: {
+ t: getAndroidComponentId('AndroidText'),
+ c: 'Hello',
+ },
+ })
+})
+
+test('fails loudly for unknown Android component names', () => {
+ const Unknown = createVoltraComponent('UnknownAndroidWidget')
+
+ assert.throws(
+ () =>
+ renderAndroidWidgetToJson([
+ {
+ size: { width: 100, height: 100 },
+ content: React.createElement(Unknown, null),
+ },
+ ]),
+ {
+ message: /Unknown Android component name: "UnknownAndroidWidget"/,
+ }
+ )
+})
diff --git a/packages/android/tsconfig.typecheck.json b/packages/android/tsconfig.typecheck.json
index 83a4c4cc..fd44b930 100644
--- a/packages/android/tsconfig.typecheck.json
+++ b/packages/android/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/core/README.md b/packages/core/README.md
index e4c54e60..c0e885ce 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -13,7 +13,7 @@ For installation and setup instructions, see the Voltra documentation: [use-volt
## 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.
@@ -22,9 +22,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/core?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/core?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/core
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ./CONTRIBUTING.md
diff --git a/packages/core/package.json b/packages/core/package.json
index aefa0adb..e84697d6 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -22,7 +22,8 @@
"build": "node ../../scripts/build-package.mjs packages/core",
"clean": "rm -rf build",
"lint": "oxlint src",
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
+ "test": "node --test"
},
"dependencies": {
"react-is": "^19.2.0"
diff --git a/packages/core/src/renderer/renderer.ts b/packages/core/src/renderer/renderer.ts
index e42a3f15..ae3f9aa3 100644
--- a/packages/core/src/renderer/renderer.ts
+++ b/packages/core/src/renderer/renderer.ts
@@ -154,8 +154,25 @@ function renderNodeInternal(element: ReactNode, context: VoltraRenderingContext)
if (isLazy(element)) {
const lazyElement = element as ReactElement>>
- const { lazy } = lazyElement.type as unknown as { lazy: () => ReactNode }
- return renderNode(lazy(), context)
+ const lazyType = lazyElement.type as unknown as {
+ _init?: (payload: unknown) => ComponentType
+ _payload?: unknown
+ }
+
+ if (typeof lazyType._init !== 'function') {
+ throw new Error('Lazy component could not be resolved by the Voltra renderer.')
+ }
+
+ try {
+ const resolvedType = lazyType._init(lazyType._payload)
+ return renderNode({ ...lazyElement, type: resolvedType }, context)
+ } catch (error) {
+ if (error instanceof Promise) {
+ throw new Error('Lazy component suspended! Voltra does not support Suspense/Promises.')
+ }
+
+ throw error
+ }
}
if (isContextProvider(element)) {
diff --git a/packages/core/test/renderer.test.js b/packages/core/test/renderer.test.js
new file mode 100644
index 00000000..cc33a696
--- /dev/null
+++ b/packages/core/test/renderer.test.js
@@ -0,0 +1,173 @@
+const test = require('node:test')
+const assert = require('node:assert/strict')
+const React = require('react')
+
+const {
+ VOLTRA_PAYLOAD_VERSION,
+ createVoltraComponent,
+ createVoltraRenderer,
+ renderVariantToJson,
+} = require('../build/cjs/index.js')
+
+const View = createVoltraComponent('View')
+const Text = createVoltraComponent('Text')
+
+const componentIds = {
+ View: 1,
+ Text: 2,
+}
+
+const componentRegistry = {
+ getComponentId(name) {
+ const id = componentIds[name]
+
+ if (id === undefined) {
+ throw new Error(`Unknown component: ${name}`)
+ }
+
+ return id
+ },
+}
+
+function renderRoots(entries) {
+ const renderer = createVoltraRenderer(componentRegistry)
+
+ for (const [name, node] of entries) {
+ renderer.addRootNode(name, node)
+ }
+
+ return renderer.render()
+}
+
+test('renders stable payloads for equivalent trees and encodes props compactly', () => {
+ const tree = React.createElement(
+ View,
+ {
+ id: 'root',
+ marginTop: 12,
+ style: [{ padding: 8 }, null, { padding: 16, opacity: 0.5 }],
+ },
+ React.createElement(Text, null, 'Hello ', 42, false, null, undefined, 'world')
+ )
+
+ const first = renderRoots([['content', tree]])
+ const second = renderRoots([['content', tree]])
+
+ assert.deepStrictEqual(first, second)
+ assert.deepStrictEqual(first, {
+ v: VOLTRA_PAYLOAD_VERSION,
+ content: {
+ t: 1,
+ i: 'root',
+ c: { t: 2, c: 'Hello 42world' },
+ p: { mt: 12, s: 0 },
+ },
+ s: [{ pad: 16, op: 0.5 }],
+ })
+})
+
+test('supports fragments, function components, memo, forwardRef, lazy, and context', () => {
+ const NameContext = React.createContext('fallback')
+
+ function Greeting() {
+ return React.createElement(NameContext.Consumer, null, (value) => React.createElement(Text, null, 'Hello ', value))
+ }
+
+ const MemoGreeting = React.memo(Greeting)
+ const ForwardedGreeting = React.forwardRef(function ForwardedGreeting(_props, _ref) {
+ return React.createElement(MemoGreeting)
+ })
+ const LazyGreeting = {
+ $$typeof: Symbol.for('react.lazy'),
+ _payload: ForwardedGreeting,
+ _init: (payload) => payload,
+ }
+
+ const rendered = renderVariantToJson(
+ React.createElement(
+ NameContext.Provider,
+ { value: 'Voltra' },
+ React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(View, null, React.createElement(LazyGreeting)),
+ React.createElement(
+ NameContext.Provider,
+ { value: 'Nested' },
+ React.createElement(View, null, React.createElement(Greeting))
+ )
+ )
+ ),
+ componentRegistry
+ )
+
+ assert.deepStrictEqual(rendered, [
+ { t: 1, c: { t: 2, c: 'Hello Voltra' } },
+ { t: 1, c: { t: 2, c: 'Hello Nested' } },
+ ])
+})
+
+test('deduplicates repeated element objects and reuses stylesheet entries', () => {
+ const sharedChild = React.createElement(Text, { style: { color: 'red' } }, 'shared')
+ const rendered = renderRoots([
+ ['small', sharedChild],
+ ['large', sharedChild],
+ ])
+
+ assert.deepStrictEqual(rendered, {
+ v: VOLTRA_PAYLOAD_VERSION,
+ small: { $r: 0 },
+ large: { $r: 0 },
+ e: [{ t: 2, c: 'shared', p: { s: 0 } }],
+ s: [{ c: 'red' }],
+ })
+})
+
+test('rejects raw text outside text components', () => {
+ assert.throws(
+ () => renderVariantToJson(React.createElement(View, null, 'nope'), componentRegistry),
+ /Strings are only allowed as children of Text components/
+ )
+})
+
+test('rejects unsupported host and class components', () => {
+ class LegacyComponent extends React.Component {
+ render() {
+ return React.createElement(Text, null, 'legacy')
+ }
+ }
+
+ assert.throws(() => renderVariantToJson(React.createElement('div'), componentRegistry), /Host component "div"/)
+ assert.throws(
+ () => renderVariantToJson(React.createElement(LegacyComponent), componentRegistry),
+ /Class components are not supported/
+ )
+})
+
+test('rejects strict mode, suspense, profiler, and portals with useful errors', () => {
+ assert.throws(
+ () =>
+ renderVariantToJson(React.createElement(React.StrictMode, null, React.createElement(View)), componentRegistry),
+ /Strict mode is not supported/
+ )
+ assert.throws(
+ () =>
+ renderVariantToJson(
+ React.createElement(React.Suspense, { fallback: null }, React.createElement(View)),
+ componentRegistry
+ ),
+ /Suspense is not supported/
+ )
+ assert.throws(
+ () =>
+ renderVariantToJson(
+ React.createElement(React.Profiler, { id: 'profile', onRender() {} }, React.createElement(View)),
+ componentRegistry
+ ),
+ /Profiler is not supported/
+ )
+ assert.throws(
+ () => renderVariantToJson({ type: Symbol.for('react.portal'), props: {} }, componentRegistry),
+ /Portal is not supported/
+ )
+})
diff --git a/packages/core/tsconfig.typecheck.json b/packages/core/tsconfig.typecheck.json
index 83a4c4cc..fd44b930 100644
--- a/packages/core/tsconfig.typecheck.json
+++ b/packages/core/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/expo-plugin/README.md b/packages/expo-plugin/README.md
index fe20a7de..7c00f6b5 100644
--- a/packages/expo-plugin/README.md
+++ b/packages/expo-plugin/README.md
@@ -1,34 +1,14 @@
-
+# @use-voltra/expo-plugin
-### Voltra Expo Plugin
+Shared build-time utilities for Voltra Expo config plugins:
-[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
+- Localized label helpers and locale fallback (`localePick`)
+- Widget config validation primitives
+- Initial-state prerendering pipeline
-`@use-voltra/expo-plugin` contains the Expo config plugin used by Voltra to configure iOS Live Activities, widget extensions, and Android widgets during `expo prebuild`.
+Platform-specific config plugins ship with:
-> [!WARNING]
-> Most apps should install `voltra` instead of using this package directly.
+- [`@use-voltra/ios-client`](../ios-client) — Live Activities, widget extension, push
+- [`@use-voltra/android-client`](../android-client) — home screen widgets
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
-
-## Attribution
-
-This plugin was derived from [expo-live-activity](https://github.com/software-mansion-labs/expo-live-activity) by Software Mansion.
-
-## 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].
-
-If you think it's cool, please star it 🌟. This project will always remain free to use.
-
-[Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
-
-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]: 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
-[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
-[prs-welcome]: ./CONTRIBUTING.md
+See [use-voltra.dev](https://use-voltra.dev) for setup.
diff --git a/packages/expo-plugin/package.json b/packages/expo-plugin/package.json
index 28b3f497..e480797b 100644
--- a/packages/expo-plugin/package.json
+++ b/packages/expo-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@use-voltra/expo-plugin",
"version": "1.4.1",
- "description": "Expo config plugin for Voltra",
+ "description": "Shared utilities for Voltra Expo config plugins",
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
@@ -26,19 +26,12 @@
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
},
"dependencies": {
- "@babel/core": "^7.27.4",
- "@expo/config-plugins": "~10.1.2",
- "@expo/plist": "^0.3.5",
- "dedent": "^1.7.1",
- "vd-tool": "^4.0.2",
- "xcode": "^3.0.1"
+ "@babel/core": "^7.27.4"
},
"keywords": [
"voltra",
"expo",
- "plugin",
- "ios",
- "android"
+ "plugin"
],
"author": "Saúl Sharma (https://x.com/saul_sharma), Szymon Chmal (https://x.com/chmalszymon)",
"repository": {
diff --git a/packages/expo-plugin/src/android/index.ts b/packages/expo-plugin/src/android/index.ts
deleted file mode 100644
index d0778129..00000000
--- a/packages/expo-plugin/src/android/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ConfigPlugin, withPlugins } from '@expo/config-plugins'
-
-import type { AndroidPluginProps } from '../types'
-import { validateAndroidWidgetConfig } from '../validation'
-import { generateAndroidWidgetFiles } from './files'
-import { configureAndroidManifest } from './manifest'
-
-/**
- * Main Android configuration plugin.
- *
- * This orchestrates all Android-related configuration in the correct order:
- * 1. Validate widget configurations with file existence checks
- * 2. Generate widget files (Kotlin receivers, XML metadata, resources)
- * 3. Configure AndroidManifest (receiver entries)
- */
-export const withAndroid: ConfigPlugin = (config, props) => {
- const { enableNotifications, widgets, userImagesPath, fonts } = props
-
- if (!config.android?.package) {
- throw new Error(
- 'Voltra config plugin requires expo.android.package to be set in app.json/app.config.* to configure Android widgets.'
- )
- }
-
- // Get project root from modRequest if available, otherwise validation will skip file checks
- const projectRoot = (config as any).modRequest?.projectRoot
-
- // Validate Android widgets with file existence checks
- widgets.forEach((widget) => validateAndroidWidgetConfig(widget, projectRoot))
-
- return withPlugins(config, [
- // 1. Generate widget files (must run first so files exist)
- [generateAndroidWidgetFiles, { widgets, userImagesPath, fonts }],
-
- // 2. Configure AndroidManifest (must run after files are generated)
- [configureAndroidManifest, { enableNotifications, widgets }],
- ])
-}
diff --git a/packages/expo-plugin/src/constants.ts b/packages/expo-plugin/src/constants.ts
index b28ec381..1e87c0d4 100644
--- a/packages/expo-plugin/src/constants.ts
+++ b/packages/expo-plugin/src/constants.ts
@@ -1,60 +1,5 @@
-import type { WidgetFamily } from './types'
-
-/**
- * Constants for the Voltra plugin
- */
-
-// ============================================================================
-// iOS Constants
-// ============================================================================
-
-export const IOS = {
- /** Minimum iOS deployment target version */
- DEPLOYMENT_TARGET: '17.0',
-
- /** Swift language version */
- SWIFT_VERSION: '5.0',
-
- /** Target device families (1 = iPhone, 2 = iPad) */
- DEVICE_FAMILY: '1,2',
-
- /** Last Swift migration version for Xcode */
- LAST_SWIFT_MIGRATION: 1250,
-} as const
-
-// ============================================================================
-// Path Constants
-// ============================================================================
-
-/** Default path for user-provided widget images */
-export const DEFAULT_USER_IMAGES_PATH = './assets/voltra'
-
-/** Default path for user-provided Android widget images */
-export const DEFAULT_ANDROID_USER_IMAGES_PATH = './assets/voltra-android'
-
-// ============================================================================
-// Widget Constants
-// ============================================================================
-
-/** Maximum image size in bytes for Live Activities (4KB limit) */
-export const MAX_IMAGE_SIZE_BYTES = 4096
-
-/** Supported image extensions for widget assets */
-export const SUPPORTED_IMAGE_EXTENSIONS = /\.(png|jpg|jpeg)$/i
-
-/** Default widget families when not specified */
-export const DEFAULT_WIDGET_FAMILIES: WidgetFamily[] = ['systemSmall', 'systemMedium', 'systemLarge']
-
-/** Maps JS widget family names to SwiftUI WidgetFamily enum cases */
-export const WIDGET_FAMILY_MAP: Record = {
- systemSmall: '.systemSmall',
- systemMedium: '.systemMedium',
- systemLarge: '.systemLarge',
- systemExtraLarge: '.systemExtraLarge',
- accessoryCircular: '.accessoryCircular',
- accessoryRectangular: '.accessoryRectangular',
- accessoryInline: '.accessoryInline',
-}
-
/** Extensions to try when resolving module paths for pre-rendering */
export const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '']
+
+/** Maximum image size in bytes for widget / Live Activity assets (4KB limit) */
+export const MAX_IMAGE_SIZE_BYTES = 4096
diff --git a/packages/expo-plugin/src/index.ts b/packages/expo-plugin/src/index.ts
index 5ff78226..ad57c946 100644
--- a/packages/expo-plugin/src/index.ts
+++ b/packages/expo-plugin/src/index.ts
@@ -1,98 +1,15 @@
-import { IOSConfig } from 'expo/config-plugins'
-
-import { withAndroid } from './android'
-import { IOS } from './constants'
-import { withIOS, withPushNotifications } from './ios'
-import { withIOS as withIOSWidget } from './ios-widget'
-import type { VoltraConfigPlugin } from './types'
-import { ensureURLScheme } from './utils/urlScheme'
-import { validateProps } from './validation'
-
-/**
- * Main Voltra config plugin.
- *
- * This plugin configures your Expo app for:
- * - Live Activities (Dynamic Island + Lock Screen)
- * - Home Screen Widgets (iOS and Android)
- * - Push Notifications for Live Activities (optional)
- */
-const withVoltra: VoltraConfigPlugin = (config, props = {}) => {
- // Validate props at entry point
- validateProps(props)
-
- // Configure iOS if bundleIdentifier is available
- if (config.ios?.bundleIdentifier) {
- // Use deploymentTarget from props if provided, otherwise fall back to default
- const deploymentTarget = props.deploymentTarget || IOS.DEPLOYMENT_TARGET
- // Use custom targetName if provided, otherwise fall back to default "{AppName}LiveActivity"
- const targetName = props.targetName || `${IOSConfig.XcodeUtils.sanitizedName(config.name)}LiveActivity`
- const bundleIdentifier = `${config.ios.bundleIdentifier}.${targetName}`
-
- // Extract version and buildNumber from config, defaulting to Expo defaults
- const version = config.version || '1.0.0'
- const buildNumber = config.ios?.buildNumber || '1'
-
- // Ensure URL scheme is set for widget deep linking
- config = ensureURLScheme(config)
-
- // Derive a default keychainGroup from bundle identifier when any widget uses server-driven updates
- const hasServerDrivenWidgets = props?.widgets?.some((w) => w.serverUpdate) ?? false
- const keychainGroup =
- props?.keychainGroup ??
- (hasServerDrivenWidgets ? `$(AppIdentifierPrefix)${config.ios?.bundleIdentifier}` : undefined)
-
- // Configure iOS main app (Info.plist, entitlements, EAS)
- config = withIOS(config, {
- groupIdentifier: props?.groupIdentifier,
- widgetIds: props?.widgets && props.widgets.length > 0 ? props.widgets.map((w) => w.id) : undefined,
- widgets: props?.widgets,
- keychainGroup,
- })
-
- // Configure iOS widget extension (files, xcode, podfile, plist, eas)
- config = withIOSWidget(config, {
- targetName,
- bundleIdentifier,
- deploymentTarget,
- widgets: props?.widgets,
- version,
- buildNumber,
- ...(props?.groupIdentifier ? { groupIdentifier: props.groupIdentifier } : {}),
- ...(keychainGroup ? { keychainGroup } : {}),
- ...(props?.fonts ? { fonts: props.fonts } : {}),
- })
- }
-
- // Apply Android configuration (files, manifest)
- if (props.android) {
- config = withAndroid(config, {
- enableNotifications: props.android.enableNotifications,
- widgets: props.android.widgets ?? [],
- ...(props?.fonts ? { fonts: props.fonts } : {}),
- })
- }
-
- // Optionally enable push notifications
- if (props.enablePushNotifications) {
- config = withPushNotifications(config)
- }
-
- return config
-}
-
-export default withVoltra
-
-// Re-export public types
-export type {
- AndroidPluginConfig,
- AndroidWidgetConfig,
- AndroidWidgetServerUpdateConfig,
- ConfigPluginProps,
- VoltraConfigPlugin,
- WidgetConfig,
- WidgetFamily,
- WidgetInitialStatePath,
- WidgetLabel,
- WidgetLocalizedCopy,
- WidgetServerUpdateConfig,
-} from './types'
+export { MAX_IMAGE_SIZE_BYTES, MODULE_EXTENSIONS } from './constants'
+export type { WidgetInitialStatePath, WidgetLabel, WidgetLocalizedCopy } from './types'
+export {
+ assertValidLocaleKey,
+ validateHomeScreenWidgetId,
+ validateInitialStatePath,
+ validateWidgetLabel,
+} from './validation'
+export { addApplicationGroupsEntitlement } from './utils/entitlements'
+export { resolveFontPaths } from './utils/fonts'
+export { normalizeLocaleTag, pickLocalizedValue } from './utils/localePick'
+export { logger } from './utils/logger'
+export type { PrerenderableWidget, PrerenderedWidgetStates, WidgetRenderer } from './utils/prerender'
+export { prerenderWidgetState } from './utils/prerender'
+export { isWidgetLocalizedMap, widgetLabelEnglish } from './utils/widgetLabel'
diff --git a/packages/expo-plugin/src/types.ts b/packages/expo-plugin/src/types.ts
index dacfe5da..55f1c958 100644
--- a/packages/expo-plugin/src/types.ts
+++ b/packages/expo-plugin/src/types.ts
@@ -1,13 +1,8 @@
-import { ConfigPlugin } from '@expo/config-plugins'
-
/**
- * Type definitions for the Voltra plugin
+ * Types shared by iOS and Android Voltra config plugins.
+ * Platform widget config types live in each client's expo-plugin package.
*/
-// ============================================================================
-// Widget Types
-// ============================================================================
-
/**
* Per-locale strings for widget picker/gallery labels (`displayName`, `description`).
* Keys should be BCP-47-style locale tags (e.g. `en`, `pl`, `pt-BR`). Plain `string` is still allowed for a single-language setup.
@@ -21,294 +16,3 @@ export type WidgetLabel = string | WidgetLocalizedCopy
* Each path must point to a module that exports the widget variants / default export for prerendering.
*/
export type WidgetInitialStatePath = string | WidgetLocalizedCopy
-
-/**
- * Supported widget size families
- */
-export type WidgetFamily =
- | 'systemSmall'
- | 'systemMedium'
- | 'systemLarge'
- | 'systemExtraLarge'
- | 'accessoryCircular'
- | 'accessoryRectangular'
- | 'accessoryInline'
-
-/**
- * Configuration for a single home screen widget
- */
-export interface WidgetConfig {
- /**
- * Unique identifier for the widget (used as the widget kind and in JS API)
- * Must be alphanumeric with underscores only
- */
- id: string
- /**
- * Display name shown in the widget gallery.
- * For locale maps, keys must be BCP-47-like (`en`, `pl`, `pt-BR`, `zh-Hans`); include an English locale when possible so defaults align with Android `values/` and iOS fallbacks.
- */
- displayName: WidgetLabel
- /**
- * Description shown in the widget gallery (same rules as `displayName`).
- */
- description: WidgetLabel
- /**
- * Supported widget sizes
- * @default ['systemSmall', 'systemMedium', 'systemLarge']
- */
- supportedFamilies?: WidgetFamily[]
- /**
- * Path to a file that default exports a WidgetVariants object for initial widget state (or a locale map of paths).
- * This will be pre-rendered at build time and bundled into the iOS app.
- */
- initialStatePath?: WidgetInitialStatePath
- /**
- * Configuration for server-driven widget updates.
- * When configured, the widget will periodically fetch new content from a remote server
- * running Voltra SSR, without requiring the user to open the app.
- */
- serverUpdate?: WidgetServerUpdateConfig
-}
-
-/**
- * Configuration for server-driven widget updates.
- * Enables widgets to pull updates from a remote Voltra SSR service.
- */
-export interface WidgetServerUpdateConfig {
- /**
- * The URL of the Voltra SSR endpoint that returns widget JSON.
- * The widget ID and family will be appended as query parameters.
- */
- url: string
- /**
- * How often the widget should fetch updates, in minutes.
- * iOS WidgetKit may throttle requests; minimum effective interval is ~15 minutes.
- * @default 15
- */
- intervalMinutes?: number
- /**
- * Whether to show a native refresh button in the top-right corner of the widget.
- * When tapped, triggers an immediate server update.
- * @default false
- */
- refresh?: boolean
-}
-
-/**
- * Structure describing the files in a widget extension target.
- * Used for configuring Xcode build phases and groups.
- */
-export interface WidgetFiles {
- swiftFiles: string[]
- entitlementFiles: string[]
- plistFiles: string[]
- assetDirectories: string[]
- intentFiles: string[]
- /** Paths relative to the widget extension root (e.g. en.lproj/VoltraWidgets.strings) */
- localizedStringResources: string[]
-}
-
-// ============================================================================
-// Android Types
-// ============================================================================
-
-/**
- * Configuration for a single Android widget
- */
-export interface AndroidWidgetConfig {
- /**
- * Unique identifier for the widget
- */
- id: string
- /**
- * Display name shown in the widget picker (same localization rules as iOS `widgets[].displayName`).
- */
- displayName: WidgetLabel
- /**
- * Description shown in the widget picker (same localization rules as iOS `widgets[].description`).
- */
- description: WidgetLabel
- /**
- * Minimum width in dp. If provided, takes precedence over minCellWidth.
- */
- minWidth?: number
- /**
- * Minimum height in dp. If provided, takes precedence over minCellHeight.
- */
- minHeight?: number
- /**
- * Minimum width in cells. Used to derive minWidth if not provided: (N * 70) - 30
- */
- minCellWidth?: number
- /**
- * Minimum height in cells. Used to derive minHeight if not provided: (N * 70) - 30
- */
- minCellHeight?: number
- /**
- * Target cell width (Android 12+, 1-5 cells)
- */
- targetCellWidth: number
- /**
- * Target cell height (Android 12+, 1-5 cells)
- */
- targetCellHeight: number
- /**
- * Whether the widget can be resized
- * @default 'horizontal|vertical'
- */
- resizeMode?: 'none' | 'horizontal' | 'vertical' | 'horizontal|vertical'
- /**
- * Widget category
- * @default 'home_screen'
- */
- widgetCategory?: 'home_screen' | 'keyguard' | 'home_screen|keyguard'
- /**
- * Path to a file that default exports a WidgetVariants object for initial widget state (or a locale map of paths).
- * This will be pre-rendered at build time and bundled into the app.
- */
- initialStatePath?: WidgetInitialStatePath
- /**
- * Configuration for server-driven widget updates.
- * When configured, the widget will periodically fetch new content from a remote server
- * running Voltra SSR, without requiring the user to open the app.
- */
- serverUpdate?: AndroidWidgetServerUpdateConfig
- /**
- * Path to preview image for widget picker (PNG/JPG/WebP).
- * Sets android:previewImage attribute. Works on all Android versions.
- * On Android 12+, combine with previewLayout for better results.
- */
- previewImage?: string
- /**
- * Path to custom XML layout for RemoteViews preview (Android 12+).
- * Sets android:previewLayout attribute for scalable previews.
- * If not provided but previewImage is set, an auto-layout will be generated.
- */
- previewLayout?: string
-}
-
-/**
- * Configuration for server-driven Android widget updates.
- */
-export interface AndroidWidgetServerUpdateConfig {
- /**
- * The URL of the Voltra SSR endpoint that returns widget JSON.
- * The widget ID will be appended as a query parameter.
- * Example: "https://api.example.com/widgets/render"
- */
- url: string
- /**
- * How often the widget should fetch updates, in minutes.
- * Uses WorkManager PeriodicWorkRequest; minimum interval is 15 minutes.
- * @default 60
- */
- intervalMinutes?: number
- /**
- * Whether to show a native refresh button in the top-right corner of the widget.
- * When tapped, triggers an immediate server update.
- * @default false
- */
- refresh?: boolean
-}
-
-/**
- * Android-specific plugin configuration
- */
-export interface AndroidPluginConfig {
- /**
- * Enable Android notification-related manifest plumbing used by Voltra features
- * such as Live Updates.
- */
- enableNotifications?: boolean
- /**
- * Android home screen widgets
- * Separate from iOS widgets to allow platform-specific configurations
- */
- widgets?: AndroidWidgetConfig[]
-}
-
-// ============================================================================
-// Plugin Types
-// ============================================================================
-
-/**
- * Props for the Voltra config plugin
- */
-export interface ConfigPluginProps {
- /**
- * Enable push notification support for Live Activities
- */
- enablePushNotifications?: boolean
- /**
- * App group identifier for sharing data between app and widget extension
- */
- groupIdentifier?: string
- /**
- * Configuration for iOS home screen widgets (uses WidgetFamily sizing)
- * Each widget will be available in the widget gallery
- */
- widgets?: WidgetConfig[]
- /**
- * iOS deployment target version for the widget extension
- * If not provided, will use the main app's deployment target or fall back to the default
- */
- deploymentTarget?: string
- /**
- * Custom target name for the widget extension
- * If not provided, defaults to "{AppName}LiveActivity"
- * Useful for matching existing provisioning profiles or credentials
- */
- targetName?: string
- /**
- * Custom fonts to include in the Live Activity extension.
- * Provide an array of font module specifiers, file paths, or directories containing fonts.
- * Supports .ttf, .otf, .woff, and .woff2 formats.
- *
- * This is equivalent to expo-font but for the Live Activity extension.
- * @see https://docs.expo.dev/versions/latest/sdk/font/
- */
- fonts?: string[]
- /**
- * Keychain Access Group for sharing credentials between the main app and widget extension.
- * Required when using server-driven widget updates with authentication.
- * This should match a Keychain Sharing capability group configured in your Apple Developer account.
- * Example: "$(AppIdentifierPrefix)com.example.shared"
- *
- * If not provided and any widget has `serverUpdate` configured, defaults to
- * `$(AppIdentifierPrefix)` (the app's own keychain access group).
- */
- keychainGroup?: string
- /**
- * Android-specific configuration
- */
- android?: AndroidPluginConfig
-}
-
-/**
- * The main Voltra config plugin type
- */
-export type VoltraConfigPlugin = ConfigPlugin
-
-/**
- * Props passed to iOS-related plugins
- */
-export interface IOSPluginProps {
- targetName: string
- bundleIdentifier: string
- deploymentTarget: string
- widgets?: WidgetConfig[]
- groupIdentifier?: string
- projectRoot: string
- platformProjectRoot: string
- fonts?: string[]
-}
-
-/**
- * Props passed to Android-related plugins
- */
-export interface AndroidPluginProps {
- enableNotifications?: boolean
- widgets: AndroidWidgetConfig[]
- userImagesPath?: string
- fonts?: string[]
-}
diff --git a/packages/expo-plugin/src/validation.ts b/packages/expo-plugin/src/validation.ts
index 76195afd..9b78a102 100644
--- a/packages/expo-plugin/src/validation.ts
+++ b/packages/expo-plugin/src/validation.ts
@@ -1,30 +1,19 @@
import * as fs from 'fs'
import * as path from 'path'
-import type {
- AndroidWidgetConfig,
- ConfigPluginProps,
- WidgetConfig,
- WidgetFamily,
- WidgetInitialStatePath,
-} from './types'
-
-/**
- * Validation functions for the Voltra plugin
- */
+import type { WidgetInitialStatePath } from './types'
/** Widget id: Swift / Kotlin identifier fragment and Android XML token fragment */
const WIDGET_ID_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
/**
* Locale keys in app.json become iOS `.lproj` folder names and Android `values-*` qualifiers (after mapping).
- * Restrict to safe, BCP-47-like tags: no slashes, spaces, or exotic punctuation.
*/
const LOCALE_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$/
const MAX_LOCALE_KEY_LENGTH = 32
-function validateHomeScreenWidgetId(widgetId: unknown): asserts widgetId is string {
+export function validateHomeScreenWidgetId(widgetId: unknown): asserts widgetId is string {
if (!widgetId || typeof widgetId !== 'string') {
throw new Error('Widget ID is required and must be a string')
}
@@ -37,7 +26,7 @@ function validateHomeScreenWidgetId(widgetId: unknown): asserts widgetId is stri
}
}
-function assertValidLocaleKey(localeKey: string, widgetId: string, fieldName: string): void {
+export function assertValidLocaleKey(localeKey: string, widgetId: string, fieldName: string): void {
if (localeKey.trim() !== localeKey) {
throw new Error(
`Widget '${widgetId}': ${fieldName} locale key '${localeKey}' must not have leading or trailing whitespace`
@@ -62,7 +51,7 @@ function assertValidLocaleKey(localeKey: string, widgetId: string, fieldName: st
}
}
-function validateWidgetLabel(value: unknown, widgetId: string, fieldName: string): void {
+export function validateWidgetLabel(value: unknown, widgetId: string, fieldName: string): void {
if (typeof value === 'string') {
if (!value.trim()) {
throw new Error(`Widget '${widgetId}': ${fieldName} is required`)
@@ -157,204 +146,3 @@ export function validateInitialStatePath(
assertPathExists(v, `.${locale}`)
}
}
-
-// ============================================================================
-// iOS Widget Validation
-// ============================================================================
-
-const VALID_FAMILIES: Set = new Set([
- 'systemSmall',
- 'systemMedium',
- 'systemLarge',
- 'systemExtraLarge',
- 'accessoryCircular',
- 'accessoryRectangular',
- 'accessoryInline',
-])
-
-/**
- * Validates a widget configuration.
- * Throws an error if validation fails.
- */
-export function validateWidgetConfig(widget: WidgetConfig): void {
- validateHomeScreenWidgetId(widget.id)
-
- validateWidgetLabel(widget.displayName, widget.id, 'displayName')
- validateWidgetLabel(widget.description, widget.id, 'description')
- /** File existence is checked when `projectRoot` is available (e.g. Android prebuild). */
- validateInitialStatePath(widget.initialStatePath, widget.id)
-
- // Validate supported families if provided
- if (widget.supportedFamilies) {
- if (!Array.isArray(widget.supportedFamilies)) {
- throw new Error(`Widget '${widget.id}': supportedFamilies must be an array`)
- }
-
- for (const family of widget.supportedFamilies) {
- if (!VALID_FAMILIES.has(family)) {
- throw new Error(
- `Widget '${widget.id}': Invalid widget family '${family}'. ` +
- `Valid families are: ${Array.from(VALID_FAMILIES).join(', ')}`
- )
- }
- }
- }
-}
-
-// ============================================================================
-// Android Widget Validation
-// ============================================================================
-
-/**
- * Validates an Android widget configuration.
- * Throws an error if validation fails.
- */
-export function validateAndroidWidgetConfig(widget: AndroidWidgetConfig, projectRoot?: string): void {
- validateHomeScreenWidgetId(widget.id)
-
- validateWidgetLabel(widget.displayName, widget.id, 'displayName')
- validateWidgetLabel(widget.description, widget.id, 'description')
- validateInitialStatePath(widget.initialStatePath, widget.id, projectRoot)
-
- // Validate targetCellWidth
- if (typeof widget.targetCellWidth !== 'number') {
- throw new Error(`Widget '${widget.id}': targetCellWidth is required and must be a number`)
- }
- if (!Number.isInteger(widget.targetCellWidth) || widget.targetCellWidth < 1) {
- throw new Error(`Widget '${widget.id}': targetCellWidth must be a positive integer (typically 1-5)`)
- }
-
- // Validate targetCellHeight
- if (typeof widget.targetCellHeight !== 'number') {
- throw new Error(`Widget '${widget.id}': targetCellHeight is required and must be a number`)
- }
- if (!Number.isInteger(widget.targetCellHeight) || widget.targetCellHeight < 1) {
- throw new Error(`Widget '${widget.id}': targetCellHeight must be a positive integer (typically 1-5)`)
- }
-
- // Validate minCellWidth if provided
- if (widget.minCellWidth !== undefined) {
- if (typeof widget.minCellWidth !== 'number' || !Number.isInteger(widget.minCellWidth) || widget.minCellWidth < 1) {
- throw new Error(`Widget '${widget.id}': minCellWidth must be a positive integer`)
- }
- }
-
- // Validate minCellHeight if provided
- if (widget.minCellHeight !== undefined) {
- if (
- typeof widget.minCellHeight !== 'number' ||
- !Number.isInteger(widget.minCellHeight) ||
- widget.minCellHeight < 1
- ) {
- throw new Error(`Widget '${widget.id}': minCellHeight must be a positive integer`)
- }
- }
-
- // Validate previewImage if provided
- if (widget.previewImage !== undefined) {
- if (typeof widget.previewImage !== 'string' || !widget.previewImage.trim()) {
- throw new Error(`Widget '${widget.id}': previewImage must be a non-empty string`)
- }
-
- const ext = path.extname(widget.previewImage).toLowerCase()
- const validImageExts = ['.png', '.jpg', '.jpeg', '.webp']
- if (!validImageExts.includes(ext)) {
- throw new Error(`Widget '${widget.id}': previewImage must be a PNG, JPG, JPEG, or WebP file. Got: ${ext}`)
- }
-
- // Check file exists if projectRoot is provided
- if (projectRoot) {
- const fullPath = path.join(projectRoot, widget.previewImage)
- if (!fs.existsSync(fullPath)) {
- throw new Error(`Widget '${widget.id}': previewImage file not found at ${widget.previewImage}`)
- }
- }
- }
-
- // Validate previewLayout if provided
- if (widget.previewLayout !== undefined) {
- if (typeof widget.previewLayout !== 'string' || !widget.previewLayout.trim()) {
- throw new Error(`Widget '${widget.id}': previewLayout must be a non-empty string`)
- }
-
- const ext = path.extname(widget.previewLayout).toLowerCase()
- if (ext !== '.xml') {
- throw new Error(`Widget '${widget.id}': previewLayout must be an XML file. Got: ${ext}`)
- }
-
- // Check file exists if projectRoot is provided
- if (projectRoot) {
- const fullPath = path.join(projectRoot, widget.previewLayout)
- if (!fs.existsSync(fullPath)) {
- throw new Error(`Widget '${widget.id}': previewLayout file not found at ${widget.previewLayout}`)
- }
- }
- }
-}
-
-// ============================================================================
-// Plugin Props Validation
-// ============================================================================
-
-/**
- * Validates the plugin props at entry point.
- * Throws an error if validation fails.
- */
-export function validateProps(props: ConfigPluginProps): void {
- // Validate group identifier format if provided
- if (props.groupIdentifier !== undefined) {
- if (typeof props.groupIdentifier !== 'string') {
- throw new Error('groupIdentifier must be a string')
- }
-
- if (!props.groupIdentifier.startsWith('group.')) {
- throw new Error(`groupIdentifier '${props.groupIdentifier}' must start with 'group.'`)
- }
- }
-
- // Validate iOS widgets if provided
- if (props.widgets !== undefined) {
- if (!Array.isArray(props.widgets)) {
- throw new Error('widgets must be an array')
- }
-
- // Check for duplicate widget IDs
- const seenIds = new Set()
- for (const widget of props.widgets) {
- validateWidgetConfig(widget)
-
- if (seenIds.has(widget.id)) {
- throw new Error(`Duplicate widget ID: '${widget.id}'`)
- }
- seenIds.add(widget.id)
- }
- }
-
- // Validate Android configuration if provided
- if (props.android !== undefined) {
- if (typeof props.android !== 'object' || props.android === null) {
- throw new Error('android configuration must be an object')
- }
-
- if (props.android.enableNotifications !== undefined && typeof props.android.enableNotifications !== 'boolean') {
- throw new Error('android.enableNotifications must be a boolean')
- }
-
- if (props.android.widgets !== undefined) {
- if (!Array.isArray(props.android.widgets)) {
- throw new Error('android.widgets must be an array')
- }
-
- // Check for duplicate widget IDs
- const seenIds = new Set()
- for (const widget of props.android.widgets) {
- validateAndroidWidgetConfig(widget)
-
- if (seenIds.has(widget.id)) {
- throw new Error(`Duplicate Android widget ID: '${widget.id}'`)
- }
- seenIds.add(widget.id)
- }
- }
- }
-}
diff --git a/packages/expo-plugin/tsconfig.typecheck.json b/packages/expo-plugin/tsconfig.typecheck.json
index 83a4c4cc..e1b6a74c 100644
--- a/packages/expo-plugin/tsconfig.typecheck.json
+++ b/packages/expo-plugin/tsconfig.typecheck.json
@@ -1,24 +1,6 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
- "module": "ES2020",
- "rootDir": "../..",
- "baseUrl": "../..",
- "paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
- "@use-voltra/android": ["packages/android/src/index.ts"],
- "@use-voltra/android-client": ["packages/android-client/src/index.ts"],
- "@use-voltra/android/client": ["packages/android-client/src/index.ts"],
- "@use-voltra/android/server": ["packages/android/src/server.ts"],
- "@use-voltra/android-server": ["packages/android-server/src/index.ts"],
- "@use-voltra/core": ["packages/core/src/index.ts"],
- "@use-voltra/ios": ["packages/ios/src/index.ts"],
- "@use-voltra/ios-client": ["packages/ios-client/src/index.ts"],
- "@use-voltra/ios/client": ["packages/ios-client/src/index.ts"],
- "@use-voltra/ios/server": ["packages/ios/src/server.ts"],
- "@use-voltra/ios-server": ["packages/ios-server/src/index.ts"],
- "@use-voltra/server": ["packages/server/src/index.ts"]
- }
+ "module": "ES2020"
}
}
diff --git a/packages/generator/README.md b/packages/generator/README.md
new file mode 100644
index 00000000..fdd3e479
--- /dev/null
+++ b/packages/generator/README.md
@@ -0,0 +1,9 @@
+# Voltra Generator
+
+This is a private workspace package.
+
+It owns Voltra's shared component schema (`data/components.json`), validation schema (`schemas/components.schema.json`), and the code generator that writes platform-specific outputs into the active `@use-voltra/*` packages.
+
+App developers should install the published runtime packages documented at [use-voltra.dev](https://use-voltra.dev/getting-started/installation), not this workspace.
+
+See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the generator workflow.
diff --git a/packages/voltra/data/components.json b/packages/generator/data/components.json
similarity index 100%
rename from packages/voltra/data/components.json
rename to packages/generator/data/components.json
diff --git a/packages/generator/generator/generate-types.ts b/packages/generator/generator/generate-types.ts
new file mode 100644
index 00000000..623eaf9b
--- /dev/null
+++ b/packages/generator/generator/generate-types.ts
@@ -0,0 +1,321 @@
+#!/usr/bin/env node
+import { execSync } from 'node:child_process'
+import * as fs from 'node:fs'
+import * as path from 'node:path'
+
+import { generateComponentIds } from './generators/component-ids'
+import { generateKotlinParameters } from './generators/kotlin-parameters'
+import { generateShortNames } from './generators/short-names'
+import { generateSwiftParameters } from './generators/swift-parameters'
+import { generateTypeScriptJSX } from './generators/typescript-jsx'
+import type { ComponentsData } from './types'
+import { validateComponentsSchema } from './validate-components'
+
+const ROOT_DIR = path.join(__dirname, '..')
+const REPO_ROOT = path.join(ROOT_DIR, '..', '..')
+
+type Logger = Pick
+
+export type GenerationPaths = {
+ rootDir: string
+ repoRoot: string
+ schemaPath: string
+ componentsDataPath: string
+ tsIosPropsOutputDir: string
+ tsAndroidPropsOutputDir: string
+ tsIosPayloadOutputDir: string
+ tsCorePayloadOutputDir: string
+ tsAndroidPayloadOutputDir: string
+ swiftParametersOutputDir: string
+ swiftSharedOutputDir: string
+ kotlinGeneratedDir: string
+ kotlinParametersOutputDir: string
+ kotlinPayloadOutputDir: string
+}
+
+type FormatScriptRunner = (scriptName: string, stepLabel: string, logger: Logger, paths: GenerationPaths) => void
+type WorkspaceFormatScriptRunner = (
+ scriptName: string,
+ workspace: string,
+ stepLabel: string,
+ logger: Logger,
+ paths: GenerationPaths
+) => void
+
+type RunGenerationOptions = {
+ paths?: GenerationPaths
+ logger?: Logger
+ runFormatScript?: FormatScriptRunner
+ runWorkspaceFormatScript?: WorkspaceFormatScriptRunner
+}
+
+export const createGenerationPaths = (rootDir: string = ROOT_DIR, repoRoot: string = REPO_ROOT): GenerationPaths => {
+ const swiftGeneratedDir = path.join(rootDir, '..', 'ios-client', 'ios', 'ui', 'Generated')
+ const androidClientNativeRoot = path.join(rootDir, '..', 'android-client', 'android')
+
+ return {
+ rootDir,
+ repoRoot,
+ schemaPath: path.join(rootDir, 'schemas/components.schema.json'),
+ componentsDataPath: path.join(rootDir, 'data/components.json'),
+ tsIosPropsOutputDir: path.join(rootDir, '..', 'ios', 'src', 'jsx', 'props'),
+ tsAndroidPropsOutputDir: path.join(rootDir, '..', 'android', 'src', 'jsx', 'props'),
+ tsIosPayloadOutputDir: path.join(rootDir, '..', 'ios', 'src', 'payload'),
+ tsCorePayloadOutputDir: path.join(rootDir, '..', 'core', 'src', 'payload'),
+ tsAndroidPayloadOutputDir: path.join(rootDir, '..', 'android', 'src', 'payload'),
+ swiftParametersOutputDir: path.join(swiftGeneratedDir, 'Parameters'),
+ swiftSharedOutputDir: path.join(rootDir, '..', 'ios-client', 'ios', 'shared'),
+ kotlinGeneratedDir: path.join(androidClientNativeRoot, 'src/main/java/voltra/generated'),
+ kotlinParametersOutputDir: path.join(androidClientNativeRoot, 'src/main/java/voltra/models/parameters'),
+ kotlinPayloadOutputDir: path.join(androidClientNativeRoot, 'src/main/java/voltra/payload'),
+ }
+}
+
+export const ensureDirectoryExists = (dir: string) => {
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true })
+ }
+}
+
+export const writeFiles = (outputDir: string, files: Record, rootDir: string = ROOT_DIR) => {
+ ensureDirectoryExists(outputDir)
+
+ for (const [filename, content] of Object.entries(files)) {
+ const filePath = path.join(outputDir, filename)
+ ensureDirectoryExists(path.dirname(filePath))
+ fs.writeFileSync(filePath, content, 'utf-8')
+ console.log(` ✓ Generated ${path.relative(rootDir, filePath)}`)
+ }
+}
+
+/** Remove previously generated files so stale platform-specific outputs are not left behind. */
+export const cleanGeneratedDirectory = (outputDir: string, shouldRemove: (filename: string) => boolean) => {
+ if (!fs.existsSync(outputDir)) {
+ return
+ }
+
+ for (const entry of fs.readdirSync(outputDir, { withFileTypes: true })) {
+ if (!entry.isFile() || !shouldRemove(entry.name)) {
+ continue
+ }
+ fs.rmSync(path.join(outputDir, entry.name))
+ }
+}
+
+export const cleanTypeScriptPropsDirectory = (outputDir: string) => {
+ cleanGeneratedDirectory(outputDir, (filename) => filename === '.generated' || filename.endsWith('.ts'))
+}
+
+export const cleanSwiftParametersDirectory = (outputDir: string) => {
+ cleanGeneratedDirectory(
+ outputDir,
+ (filename) =>
+ filename === '.generated' || filename === 'ComponentParameters.swift' || filename.endsWith('Parameters.swift')
+ )
+}
+
+export const cleanKotlinParametersDirectory = (outputDir: string) => {
+ cleanGeneratedDirectory(outputDir, (filename) => filename.endsWith('Parameters.kt'))
+}
+
+export const runFormatScript = (scriptName: string, stepLabel: string, logger: Logger, paths: GenerationPaths) => {
+ logger.log(`Step ${stepLabel}: Running npm run ${scriptName}...`)
+ try {
+ const result = execSync(`npm run ${scriptName}`, { encoding: 'utf-8', cwd: paths.rootDir })
+ if (result.trim()) {
+ logger.log(result.trim())
+ }
+ } catch (error: any) {
+ logger.warn(` Warning: npm run ${scriptName} exited with code ${error.status ?? 'unknown'}`)
+ if (error.stdout) logger.log(` stdout: ${String(error.stdout).trim()}`)
+ if (error.stderr) logger.log(` stderr: ${String(error.stderr).trim()}`)
+ }
+ logger.log('')
+}
+
+export const runWorkspaceFormatScript = (
+ scriptName: string,
+ workspace: string,
+ stepLabel: string,
+ logger: Logger,
+ paths: GenerationPaths
+) => {
+ logger.log(`Step ${stepLabel}: Running npm run ${scriptName} --workspace ${workspace}...`)
+ try {
+ execSync(`npm run ${scriptName} --workspace ${workspace}`, {
+ encoding: 'utf-8',
+ cwd: paths.repoRoot,
+ stdio: 'inherit',
+ })
+ } catch (error: any) {
+ logger.warn(
+ ` Warning: npm run ${scriptName} --workspace ${workspace} exited with code ${error.status ?? 'unknown'}`
+ )
+ if (error.stdout) logger.log(` stdout: ${String(error.stdout).trim()}`)
+ if (error.stderr) logger.log(` stderr: ${String(error.stderr).trim()}`)
+ }
+ logger.log('')
+}
+
+const fromRoot = (rootDir: string, targetPath: string) => path.relative(rootDir, targetPath) || '.'
+
+export const runGeneration = ({
+ paths = createGenerationPaths(),
+ logger = console,
+ runFormatScript: runFormatScriptImpl = runFormatScript,
+ runWorkspaceFormatScript: runWorkspaceFormatScriptImpl = runWorkspaceFormatScript,
+}: RunGenerationOptions = {}) => {
+ logger.log('🚀 Generating types from schemas...\n')
+
+ logger.log('Step 1: Validating components schema...')
+ if (
+ !validateComponentsSchema({
+ schemaPath: paths.schemaPath,
+ dataPath: paths.componentsDataPath,
+ logger,
+ })
+ ) {
+ logger.error('\n❌ Generation failed due to components validation errors')
+ process.exit(1)
+ }
+ logger.log('')
+
+ logger.log('Step 2: Loading components data...')
+ const componentsContent = fs.readFileSync(paths.componentsDataPath, 'utf-8')
+ const componentsData: ComponentsData = JSON.parse(componentsContent)
+ const componentsWithParams = componentsData.components.filter((c) => Object.keys(c.parameters).length > 0).length
+ logger.log(
+ ` ✓ Loaded ${componentsData.components.length} components (${componentsWithParams} with parameters, version ${componentsData.version})`
+ )
+ logger.log('')
+
+ logger.log('Step 3: Generating TypeScript component props types...')
+ const tsIosJsxResult = generateTypeScriptJSX(componentsData, 'ios')
+ const tsAndroidJsxResult = generateTypeScriptJSX(componentsData, 'android')
+ cleanTypeScriptPropsDirectory(paths.tsIosPropsOutputDir)
+ cleanTypeScriptPropsDirectory(paths.tsAndroidPropsOutputDir)
+ writeFiles(paths.tsIosPropsOutputDir, tsIosJsxResult.props, paths.rootDir)
+ writeFiles(paths.tsAndroidPropsOutputDir, tsAndroidJsxResult.props, paths.rootDir)
+ logger.log('')
+
+ logger.log('Step 4: Generating Swift parameter types...')
+ cleanSwiftParametersDirectory(paths.swiftParametersOutputDir)
+ const swiftParameterFiles = generateSwiftParameters(componentsData)
+ writeFiles(paths.swiftParametersOutputDir, swiftParameterFiles, paths.rootDir)
+ logger.log('')
+
+ logger.log('Step 5: Generating Kotlin parameter types...')
+ cleanKotlinParametersDirectory(paths.kotlinParametersOutputDir)
+ const kotlinParameterFiles = generateKotlinParameters(componentsData)
+ writeFiles(paths.kotlinParametersOutputDir, kotlinParameterFiles, paths.rootDir)
+ logger.log('')
+
+ logger.log('Step 6: Generating component ID mappings...')
+ const componentIdFiles = generateComponentIds(componentsData)
+ const tsIosComponentIdFiles: Record = {}
+ const tsAndroidComponentIdFiles: Record = {}
+ const swiftComponentIdFiles: Record = {}
+ const kotlinComponentIdFiles: Record = {}
+ for (const [filename, content] of Object.entries(componentIdFiles)) {
+ if (filename === 'component-ids.ts') {
+ tsIosComponentIdFiles[filename] = content
+ } else if (filename === 'android-component-ids.ts') {
+ tsAndroidComponentIdFiles['component-ids.ts'] = content
+ } else if (filename.endsWith('.swift')) {
+ swiftComponentIdFiles[filename] = content
+ } else if (filename.endsWith('.kt')) {
+ kotlinComponentIdFiles[filename] = content
+ }
+ }
+ writeFiles(paths.tsIosPayloadOutputDir, tsIosComponentIdFiles, paths.rootDir)
+ writeFiles(paths.tsAndroidPayloadOutputDir, tsAndroidComponentIdFiles, paths.rootDir)
+ writeFiles(paths.swiftSharedOutputDir, swiftComponentIdFiles, paths.rootDir)
+ writeFiles(paths.kotlinPayloadOutputDir, kotlinComponentIdFiles, paths.rootDir)
+ logger.log('')
+
+ logger.log('Step 7: Generating unified short names mappings...')
+ const shortNameFiles = generateShortNames(componentsData)
+ const tsShortNameFiles: Record = {}
+ const swiftShortNameFiles: Record = {}
+ const kotlinShortNameFiles: Record = {}
+ for (const [filename, content] of Object.entries(shortNameFiles)) {
+ if (filename.endsWith('.ts')) {
+ tsShortNameFiles[filename] = content
+ } else if (filename.endsWith('.swift')) {
+ swiftShortNameFiles[filename] = content
+ } else if (filename.endsWith('.kt')) {
+ kotlinShortNameFiles[filename] = content
+ }
+ }
+ writeFiles(paths.tsCorePayloadOutputDir, tsShortNameFiles, paths.rootDir)
+ writeFiles(paths.swiftSharedOutputDir, swiftShortNameFiles, paths.rootDir)
+ writeFiles(paths.kotlinGeneratedDir, kotlinShortNameFiles, paths.rootDir)
+ logger.log('')
+
+ runFormatScriptImpl('format:js:fix', '8', logger, paths)
+ runWorkspaceFormatScriptImpl('format:kotlin:fix', '@use-voltra/android-client', '9', logger, paths)
+ runWorkspaceFormatScriptImpl('format:swift:fix', '@use-voltra/ios-client', '10', logger, paths)
+
+ logger.log('✅ Generation complete!\n')
+ logger.log('Generated files:')
+ logger.log(
+ ` TypeScript iOS props: ${Object.keys(tsIosJsxResult.props).length} files in ${fromRoot(
+ paths.rootDir,
+ paths.tsIosPropsOutputDir
+ )}/`
+ )
+ logger.log(
+ ` TypeScript Android props: ${Object.keys(tsAndroidJsxResult.props).length} files in ${fromRoot(
+ paths.rootDir,
+ paths.tsAndroidPropsOutputDir
+ )}/`
+ )
+ logger.log(
+ ` Swift parameters: ${Object.keys(swiftParameterFiles).length} files in ${fromRoot(
+ paths.rootDir,
+ paths.swiftParametersOutputDir
+ )}/`
+ )
+ logger.log(
+ ` Kotlin parameters: ${Object.keys(kotlinParameterFiles).length} files in ${fromRoot(
+ paths.rootDir,
+ paths.kotlinParametersOutputDir
+ )}/`
+ )
+ logger.log(
+ ` Component IDs: ${Object.keys(tsIosComponentIdFiles).length} iOS TypeScript, ${
+ Object.keys(tsAndroidComponentIdFiles).length
+ } Android TypeScript, ${Object.keys(swiftComponentIdFiles).length} Swift, ${
+ Object.keys(kotlinComponentIdFiles).length
+ } Kotlin`
+ )
+ logger.log(
+ ` Short names: ${Object.keys(tsShortNameFiles).length} TypeScript in ${fromRoot(
+ paths.rootDir,
+ paths.tsCorePayloadOutputDir
+ )}/, ${Object.keys(swiftShortNameFiles).length} Swift, ${Object.keys(kotlinShortNameFiles).length} Kotlin`
+ )
+ logger.log('')
+ logger.log('Next steps:')
+ logger.log(' 1. Review generated files')
+ logger.log(
+ ` 2. Create component files manually in ${fromRoot(paths.rootDir, paths.tsIosPropsOutputDir)}/ and ${fromRoot(
+ paths.rootDir,
+ paths.tsAndroidPropsOutputDir
+ )}/ using createVoltraComponent`
+ )
+ logger.log(' 3. Run tests to ensure everything works')
+}
+
+const main = () => {
+ runGeneration()
+}
+
+if (require.main === module) {
+ try {
+ main()
+ } catch (error) {
+ console.error('❌ Generation failed:', error)
+ process.exit(1)
+ }
+}
diff --git a/packages/voltra/generator/generators/component-ids.ts b/packages/generator/generator/generators/component-ids.ts
similarity index 100%
rename from packages/voltra/generator/generators/component-ids.ts
rename to packages/generator/generator/generators/component-ids.ts
diff --git a/packages/voltra/generator/generators/kotlin-parameters.ts b/packages/generator/generator/generators/kotlin-parameters.ts
similarity index 100%
rename from packages/voltra/generator/generators/kotlin-parameters.ts
rename to packages/generator/generator/generators/kotlin-parameters.ts
diff --git a/packages/voltra/generator/generators/short-names.ts b/packages/generator/generator/generators/short-names.ts
similarity index 100%
rename from packages/voltra/generator/generators/short-names.ts
rename to packages/generator/generator/generators/short-names.ts
diff --git a/packages/voltra/generator/generators/swift-parameters.ts b/packages/generator/generator/generators/swift-parameters.ts
similarity index 100%
rename from packages/voltra/generator/generators/swift-parameters.ts
rename to packages/generator/generator/generators/swift-parameters.ts
diff --git a/packages/voltra/generator/generators/typescript-jsx.ts b/packages/generator/generator/generators/typescript-jsx.ts
similarity index 75%
rename from packages/voltra/generator/generators/typescript-jsx.ts
rename to packages/generator/generator/generators/typescript-jsx.ts
index 80d5f5e0..dd49d12c 100644
--- a/packages/voltra/generator/generators/typescript-jsx.ts
+++ b/packages/generator/generator/generators/typescript-jsx.ts
@@ -4,6 +4,17 @@ type GeneratedFiles = {
[filename: string]: string
}
+export type TypeScriptJSXPlatform = 'ios' | 'android'
+
+const isIosComponent = (component: ComponentDefinition): boolean => component.swiftAvailability !== 'Not available'
+
+const isAndroidComponent = (component: ComponentDefinition): boolean => !!component.androidAvailability
+
+const filterComponentsForPlatform = (
+ components: ComponentDefinition[],
+ platform: TypeScriptJSXPlatform
+): ComponentDefinition[] => components.filter(platform === 'ios' ? isIosComponent : isAndroidComponent)
+
const toTSType = (param: ComponentParameter): string => {
let baseType: string
if (param.type === 'component') {
@@ -54,18 +65,23 @@ import type { VoltraBaseProps } from '../baseProps'
return header + propsType + '\n'
}
-export const generateTypeScriptJSX = (data: ComponentsData): { props: GeneratedFiles; jsx: GeneratedFiles } => {
+export const generateTypeScriptJSX = (
+ data: ComponentsData,
+ platform: TypeScriptJSXPlatform
+): { props: GeneratedFiles; jsx: GeneratedFiles } => {
const propsFiles: GeneratedFiles = {}
const jsxFiles: GeneratedFiles = {}
+ const components = filterComponentsForPlatform(data.components, platform)
+ const platformLabel = platform === 'ios' ? 'iOS' : 'Android'
// Generate individual props type files
- for (const component of data.components) {
+ for (const component of components) {
const filename = `${component.name}.ts`
propsFiles[filename] = generatePropsTypeFile(component, data.version)
}
// Generate marker file
- propsFiles['.generated'] = `This directory contains auto-generated component props type files.
+ propsFiles['.generated'] = `This directory contains auto-generated ${platformLabel} component props type files.
DO NOT EDIT MANUALLY.
Generated from: data/components.json
diff --git a/packages/voltra/generator/tsconfig.json b/packages/generator/generator/tsconfig.json
similarity index 100%
rename from packages/voltra/generator/tsconfig.json
rename to packages/generator/generator/tsconfig.json
diff --git a/packages/voltra/generator/types.ts b/packages/generator/generator/types.ts
similarity index 100%
rename from packages/voltra/generator/types.ts
rename to packages/generator/generator/types.ts
diff --git a/packages/voltra/generator/validate-components.ts b/packages/generator/generator/validate-components.ts
similarity index 54%
rename from packages/voltra/generator/validate-components.ts
rename to packages/generator/generator/validate-components.ts
index b693cf9e..ec1c39ea 100644
--- a/packages/voltra/generator/validate-components.ts
+++ b/packages/generator/generator/validate-components.ts
@@ -1,5 +1,5 @@
#!/usr/bin/env node
-import Ajv from 'ajv'
+import Ajv, { type AnySchema } from 'ajv'
import * as fs from 'fs'
import * as path from 'path'
@@ -9,33 +9,35 @@ const ROOT_DIR = path.join(__dirname, '..')
const SCHEMA_PATH = path.join(ROOT_DIR, 'schemas/components.schema.json')
const DATA_PATH = path.join(ROOT_DIR, 'data/components.json')
-export function validateComponentsSchema(): boolean {
- console.log('Validating components schema...')
+type ValidationLogger = Pick
- // Load schema
- const schemaContent = fs.readFileSync(SCHEMA_PATH, 'utf-8')
- const schema = JSON.parse(schemaContent)
+type ValidateComponentsSchemaOptions = {
+ schemaPath?: string
+ dataPath?: string
+ logger?: ValidationLogger
+}
- // Load data
- const dataContent = fs.readFileSync(DATA_PATH, 'utf-8')
- const data: ComponentsData = JSON.parse(dataContent)
+export function validateComponentsData(
+ schema: unknown,
+ data: ComponentsData,
+ logger: ValidationLogger = console
+): boolean {
+ logger.log('Validating components schema...')
- // Validate with Ajv
const ajv = new Ajv({ allErrors: true })
- const validate = ajv.compile(schema)
+ const validate = ajv.compile(schema as AnySchema)
const valid = validate(data)
if (!valid) {
- console.error('❌ Schema validation failed:')
+ logger.error('❌ Schema validation failed:')
if (validate.errors) {
for (const error of validate.errors) {
- console.error(` ${error.instancePath} ${error.message}`)
+ logger.error(` ${error.instancePath} ${error.message}`)
}
}
return false
}
- // Additional validation: check for duplicate component names
const names = new Set()
const duplicates: string[] = []
@@ -47,15 +49,13 @@ export function validateComponentsSchema(): boolean {
}
if (duplicates.length > 0) {
- console.error('❌ Duplicate component names found:', duplicates.join(', '))
+ logger.error('❌ Duplicate component names found:', duplicates.join(', '))
return false
}
- // Additional validation: check for invalid parameter names
const invalidParamNames: string[] = []
for (const component of data.components) {
for (const paramName of Object.keys(component.parameters)) {
- // Parameter names should be valid JavaScript identifiers
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
invalidParamNames.push(`${component.name}.${paramName}`)
}
@@ -63,22 +63,34 @@ export function validateComponentsSchema(): boolean {
}
if (invalidParamNames.length > 0) {
- console.error('❌ Invalid parameter names (must be valid JavaScript identifiers):')
+ logger.error('❌ Invalid parameter names (must be valid JavaScript identifiers):')
for (const name of invalidParamNames) {
- console.error(` ${name}`)
+ logger.error(` ${name}`)
}
return false
}
- console.log('✅ Components schema is valid')
- console.log(` Version: ${data.version}`)
- console.log(` Components: ${data.components.length}`)
+ logger.log('✅ Components schema is valid')
+ logger.log(` Version: ${data.version}`)
+ logger.log(` Components: ${data.components.length}`)
const withParams = data.components.filter((c) => Object.keys(c.parameters).length > 0).length
- console.log(` Components with parameters: ${withParams}`)
+ logger.log(` Components with parameters: ${withParams}`)
return true
}
+export function validateComponentsSchema(options: ValidateComponentsSchemaOptions = {}): boolean {
+ const { schemaPath = SCHEMA_PATH, dataPath = DATA_PATH, logger = console } = options
+
+ const schemaContent = fs.readFileSync(schemaPath, 'utf-8')
+ const schema = JSON.parse(schemaContent)
+
+ const dataContent = fs.readFileSync(dataPath, 'utf-8')
+ const data: ComponentsData = JSON.parse(dataContent)
+
+ return validateComponentsData(schema, data, logger)
+}
+
// Run if executed directly
if (require.main === module) {
try {
diff --git a/packages/generator/package.json b/packages/generator/package.json
new file mode 100644
index 00000000..1b361520
--- /dev/null
+++ b/packages/generator/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@use-voltra/generator",
+ "private": true,
+ "version": "1.4.1",
+ "description": "Private schema and code generation tooling for Voltra",
+ "scripts": {
+ "format:js:fix": "prettier --write . ../ios/src/jsx/props ../android/src/jsx/props ../ios/src/payload ../android/src/payload ../core/src/payload",
+ "generate": "ts-node generator/generate-types.ts",
+ "test": "TS_NODE_PROJECT=generator/tsconfig.json node --test --require ts-node/register/transpile-only test/*.test.js"
+ },
+ "devDependencies": {
+ "prettier": "2.8.8",
+ "ts-node": "^10.9.0"
+ }
+}
diff --git a/packages/voltra/schemas/components.schema.json b/packages/generator/schemas/components.schema.json
similarity index 100%
rename from packages/voltra/schemas/components.schema.json
rename to packages/generator/schemas/components.schema.json
diff --git a/packages/generator/test/generator.test.js b/packages/generator/test/generator.test.js
new file mode 100644
index 00000000..a0e80c18
--- /dev/null
+++ b/packages/generator/test/generator.test.js
@@ -0,0 +1,229 @@
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const os = require('node:os')
+const path = require('node:path')
+const test = require('node:test')
+
+const { runGeneration, createGenerationPaths } = require('../generator/generate-types.ts')
+const { validateComponentsData } = require('../generator/validate-components.ts')
+
+const generatorRoot = path.resolve(__dirname, '..')
+const schema = JSON.parse(fs.readFileSync(path.join(generatorRoot, 'schemas', 'components.schema.json'), 'utf-8'))
+
+const createLogger = () => {
+ const entries = []
+ const logger = {
+ log: (...args) => entries.push({ level: 'log', text: args.join(' ') }),
+ warn: (...args) => entries.push({ level: 'warn', text: args.join(' ') }),
+ error: (...args) => entries.push({ level: 'error', text: args.join(' ') }),
+ }
+
+ return { logger, entries }
+}
+
+const createFixtureData = () => ({
+ version: '9.9.9',
+ shortNames: {
+ style: 's',
+ backgroundColor: 'bg',
+ numberOfLines: 'nol',
+ destination: 'dest',
+ text: 'txt',
+ contentAlignment: 'ca',
+ },
+ styleProperties: ['backgroundColor'],
+ components: [
+ {
+ name: 'Text',
+ description: 'Display text content',
+ swiftAvailability: 'iOS 13.0, macOS 10.15',
+ parameters: {
+ numberOfLines: {
+ type: 'number',
+ optional: true,
+ description: 'Maximum number of lines to display',
+ },
+ },
+ },
+ {
+ name: 'Link',
+ description: 'Open a destination',
+ swiftAvailability: 'iOS 14.0, macOS 11.0',
+ hasChildren: true,
+ parameters: {
+ destination: {
+ type: 'string',
+ optional: false,
+ description: 'Destination URL',
+ },
+ },
+ },
+ {
+ name: 'AndroidText',
+ description: 'Android text',
+ swiftAvailability: 'Not available',
+ androidAvailability: 'Android 12+',
+ parameters: {
+ text: {
+ type: 'string',
+ optional: false,
+ description: 'Text content',
+ },
+ },
+ },
+ {
+ name: 'AndroidBox',
+ description: 'Android box',
+ swiftAvailability: 'Not available',
+ androidAvailability: 'Android 12+',
+ hasChildren: true,
+ parameters: {
+ contentAlignment: {
+ type: 'string',
+ optional: true,
+ enum: ['center'],
+ description: 'Content alignment within the box',
+ },
+ },
+ },
+ ],
+})
+
+const writeJson = (filePath, value) => {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2))
+}
+
+test('validates component fixtures and reports useful schema errors', () => {
+ const validData = createFixtureData()
+ const validCapture = createLogger()
+ assert.equal(validateComponentsData(schema, validData, validCapture.logger), true)
+ assert.match(validCapture.entries.map((entry) => entry.text).join('\n'), /Components schema is valid/)
+
+ const missingFieldCapture = createLogger()
+ const missingFieldData = createFixtureData()
+ delete missingFieldData.components[0].parameters
+ assert.equal(validateComponentsData(schema, missingFieldData, missingFieldCapture.logger), false)
+ assert.match(
+ missingFieldCapture.entries.map((entry) => entry.text).join('\n'),
+ /must have required property 'parameters'/
+ )
+
+ const invalidEnumCapture = createLogger()
+ const invalidEnumData = createFixtureData()
+ invalidEnumData.components[0].parameters.numberOfLines.type = 'integer'
+ assert.equal(validateComponentsData(schema, invalidEnumData, invalidEnumCapture.logger), false)
+ assert.match(
+ invalidEnumCapture.entries.map((entry) => entry.text).join('\n'),
+ /must be equal to one of the allowed values/
+ )
+
+ const duplicateCapture = createLogger()
+ const duplicateData = createFixtureData()
+ duplicateData.components.push({ ...duplicateData.components[0] })
+ assert.equal(validateComponentsData(schema, duplicateData, duplicateCapture.logger), false)
+ assert.match(duplicateCapture.entries.map((entry) => entry.text).join('\n'), /Duplicate component names found: Text/)
+
+ const invalidParamCapture = createLogger()
+ const invalidParamData = createFixtureData()
+ invalidParamData.components[0].parameters['not-valid'] = invalidParamData.components[0].parameters.numberOfLines
+ assert.equal(validateComponentsData(schema, invalidParamData, invalidParamCapture.logger), false)
+ assert.match(invalidParamCapture.entries.map((entry) => entry.text).join('\n'), /Text.not-valid/)
+})
+
+test('generates synchronized artifacts into the intended directories and cleans stale files', () => {
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-generator-'))
+ const workspaceRoot = path.join(tempRoot, 'workspace')
+ const generatorTempRoot = path.join(workspaceRoot, 'packages', 'generator')
+ const fixtureData = createFixtureData()
+ const fixtureSchemaPath = path.join(generatorTempRoot, 'schemas', 'components.schema.json')
+ const fixtureDataPath = path.join(generatorTempRoot, 'data', 'components.json')
+ const paths = createGenerationPaths(generatorTempRoot, workspaceRoot)
+ const { logger, entries } = createLogger()
+ const formatCalls = []
+ const workspaceFormatCalls = []
+
+ writeJson(fixtureSchemaPath, schema)
+ writeJson(fixtureDataPath, fixtureData)
+
+ fs.mkdirSync(paths.tsIosPropsOutputDir, { recursive: true })
+ fs.writeFileSync(path.join(paths.tsIosPropsOutputDir, 'Stale.ts'), 'stale')
+ fs.writeFileSync(path.join(paths.tsIosPropsOutputDir, 'keep.txt'), 'keep')
+
+ fs.mkdirSync(paths.swiftParametersOutputDir, { recursive: true })
+ fs.writeFileSync(path.join(paths.swiftParametersOutputDir, 'OldParameters.swift'), 'stale')
+ fs.writeFileSync(path.join(paths.swiftParametersOutputDir, 'keep.txt'), 'keep')
+
+ fs.mkdirSync(paths.kotlinParametersOutputDir, { recursive: true })
+ fs.writeFileSync(path.join(paths.kotlinParametersOutputDir, 'OldParameters.kt'), 'stale')
+ fs.writeFileSync(path.join(paths.kotlinParametersOutputDir, 'keep.txt'), 'keep')
+
+ const outsidePath = path.join(tempRoot, 'outside.txt')
+ fs.writeFileSync(outsidePath, 'untouched')
+
+ runGeneration({
+ paths,
+ logger,
+ runFormatScript: (scriptName, stepLabel) => {
+ formatCalls.push({ scriptName, stepLabel })
+ },
+ runWorkspaceFormatScript: (scriptName, workspace, stepLabel) => {
+ workspaceFormatCalls.push({ scriptName, workspace, stepLabel })
+ },
+ })
+
+ assert.equal(fs.existsSync(path.join(paths.tsIosPropsOutputDir, 'Stale.ts')), false)
+ assert.equal(fs.existsSync(path.join(paths.swiftParametersOutputDir, 'OldParameters.swift')), false)
+ assert.equal(fs.existsSync(path.join(paths.kotlinParametersOutputDir, 'OldParameters.kt')), false)
+ assert.equal(fs.readFileSync(path.join(paths.tsIosPropsOutputDir, 'keep.txt'), 'utf-8'), 'keep')
+ assert.equal(fs.readFileSync(path.join(paths.swiftParametersOutputDir, 'keep.txt'), 'utf-8'), 'keep')
+ assert.equal(fs.readFileSync(path.join(paths.kotlinParametersOutputDir, 'keep.txt'), 'utf-8'), 'keep')
+ assert.equal(fs.readFileSync(outsidePath, 'utf-8'), 'untouched')
+
+ const iosTextProps = fs.readFileSync(path.join(paths.tsIosPropsOutputDir, 'Text.ts'), 'utf-8')
+ const iosLinkProps = fs.readFileSync(path.join(paths.tsIosPropsOutputDir, 'Link.ts'), 'utf-8')
+ const androidTextProps = fs.readFileSync(path.join(paths.tsAndroidPropsOutputDir, 'AndroidText.ts'), 'utf-8')
+ const androidBoxProps = fs.readFileSync(path.join(paths.tsAndroidPropsOutputDir, 'AndroidBox.ts'), 'utf-8')
+
+ assert.match(iosTextProps, /export type TextProps = VoltraBaseProps & \{/)
+ assert.match(iosTextProps, /numberOfLines\?: number/)
+ assert.match(iosLinkProps, /destination: string/)
+ assert.match(androidTextProps, /export type AndroidTextProps = VoltraBaseProps & \{/)
+ assert.match(androidTextProps, /text: string/)
+ assert.match(androidBoxProps, /contentAlignment\?: 'center'/)
+ assert.equal(fs.existsSync(path.join(paths.tsIosPropsOutputDir, 'AndroidText.ts')), false)
+ assert.equal(fs.existsSync(path.join(paths.tsAndroidPropsOutputDir, 'Text.ts')), false)
+
+ const iosComponentIds = fs.readFileSync(path.join(paths.tsIosPayloadOutputDir, 'component-ids.ts'), 'utf-8')
+ const androidComponentIds = fs.readFileSync(path.join(paths.tsAndroidPayloadOutputDir, 'component-ids.ts'), 'utf-8')
+ const swiftComponentIds = fs.readFileSync(path.join(paths.swiftSharedOutputDir, 'ComponentTypeID.swift'), 'utf-8')
+ const kotlinComponentIds = fs.readFileSync(path.join(paths.kotlinPayloadOutputDir, 'ComponentTypeID.kt'), 'utf-8')
+
+ assert.match(iosComponentIds, /'Text': 0/)
+ assert.match(iosComponentIds, /'Link': 1/)
+ assert.doesNotMatch(iosComponentIds, /AndroidText/)
+ assert.match(androidComponentIds, /'AndroidText': 0/)
+ assert.match(androidComponentIds, /'AndroidBox': 1/)
+ assert.match(swiftComponentIds, /case TEXT = 0/)
+ assert.match(swiftComponentIds, /case LINK = 1/)
+ assert.match(kotlinComponentIds, /const val TEXT = 0/)
+ assert.match(kotlinComponentIds, /const val BOX = 1/)
+
+ const tsShortNames = fs.readFileSync(path.join(paths.tsCorePayloadOutputDir, 'short-names.ts'), 'utf-8')
+ const swiftShortNames = fs.readFileSync(path.join(paths.swiftSharedOutputDir, 'ShortNames.swift'), 'utf-8')
+ const kotlinShortNames = fs.readFileSync(path.join(paths.kotlinGeneratedDir, 'ShortNames.kt'), 'utf-8')
+
+ assert.match(tsShortNames, /'backgroundColor': 'bg'/)
+ assert.match(tsShortNames, /'contentAlignment': 'ca'/)
+ assert.match(swiftShortNames, /"bg": "backgroundColor"/)
+ assert.match(swiftShortNames, /"ca": "contentAlignment"/)
+ assert.match(kotlinShortNames, /"bg" to "backgroundColor"/)
+ assert.match(kotlinShortNames, /"ca" to "contentAlignment"/)
+
+ assert.deepEqual(formatCalls, [{ scriptName: 'format:js:fix', stepLabel: '8' }])
+ assert.deepEqual(workspaceFormatCalls, [
+ { scriptName: 'format:kotlin:fix', workspace: '@use-voltra/android-client', stepLabel: '9' },
+ { scriptName: 'format:swift:fix', workspace: '@use-voltra/ios-client', stepLabel: '10' },
+ ])
+ assert.match(entries.map((entry) => entry.text).join('\n'), /Generation complete/)
+})
diff --git a/packages/ios-client/README.md b/packages/ios-client/README.md
new file mode 100644
index 00000000..3ccfc9ca
--- /dev/null
+++ b/packages/ios-client/README.md
@@ -0,0 +1,113 @@
+
+
+### Voltra for iOS — React Native client
+
+[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
+
+`@use-voltra/ios-client` is the iOS React Native package for Voltra. It re-exports the `Voltra` JSX namespace from `@use-voltra/ios` (installed automatically as a dependency) and provides runtime APIs for Live Activities and Home Screen widgets, development previews, event listeners, and the Expo config plugin.
+
+## Features
+
+- **Live Activities**: Start, update, and end activities with `useLiveActivity`, `startLiveActivity`, and related APIs.
+
+- **iOS Widgets**: Update, schedule, reload, and query widgets with `updateWidget`, `scheduleWidget`, `getActiveWidgets`, and more.
+
+- **Fast Refresh**: Hooks and previews integrate with your React Native dev workflow.
+
+- **Push & events**: Capture ActivityKit push tokens and component interactions via `addVoltraListener`.
+
+- **Image preloading**: Download remote images for use in activities and widgets with `preloadImages` and `reloadLiveActivities`.
+
+- **Expo config plugin**: Add `"@use-voltra/ios-client"` to `app.json` to generate the Live Activity extension, widget targets, and entitlements.
+
+## Documentation
+
+The documentation is available at [use-voltra.dev](https://use-voltra.dev). Relevant topics for this package:
+
+- [Installation](https://use-voltra.dev/getting-started/installation)
+- [iOS Setup](https://use-voltra.dev/ios/setup)
+- [Developing Live Activities](https://use-voltra.dev/ios/development/developing-live-activities)
+- [Developing Widgets](https://use-voltra.dev/ios/development/developing-widgets)
+- [Plugin Configuration](https://use-voltra.dev/ios/api/plugin-configuration)
+- [API Reference](https://use-voltra.dev/ios/api/configuration)
+
+## Getting started
+
+> [!NOTE]
+> Voltra isn't supported in Expo Go. Use [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/) or a native build.
+
+Install the iOS client package:
+
+```sh
+npm install @use-voltra/ios-client
+```
+
+Add the Expo plugin to your `app.json`:
+
+```json
+{
+ "expo": {
+ "plugins": [
+ [
+ "@use-voltra/ios-client",
+ {
+ "groupIdentifier": "group.your.bundle.identifier",
+ "enablePushNotifications": true
+ }
+ ]
+ ]
+ }
+}
+```
+
+Then run `npx expo prebuild --platform ios` to generate the native extension targets.
+
+See the [iOS setup guide](https://use-voltra.dev/ios/setup) for detailed instructions.
+
+## Quick example
+
+```tsx
+import { useLiveActivity, Voltra } from '@use-voltra/ios-client'
+
+export function OrderTracker({ orderId }: { orderId: string }) {
+ const ui = (
+
+ Order #{orderId}
+ Driver en route · ETA 12 min
+
+ )
+
+ const { start, update, end } = useLiveActivity(
+ { lockScreen: ui },
+ {
+ activityName: `order-${orderId}`,
+ autoStart: true,
+ deepLinkUrl: `/orders/${orderId}`,
+ }
+ )
+
+ return null
+}
+```
+
+## Platform compatibility
+
+This package targets **iOS 16.2+** (Live Activities) and supports Home Screen widgets on supported iOS versions. Import UI and runtime APIs from `@use-voltra/ios-client`. For server-side rendering, use `@use-voltra/ios-server` in your backend only.
+
+## 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].
+
+If you think it's cool, please star it 🌟. This project will always remain free to use.
+
+[Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
+
+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/@use-voltra/ios-client?style=for-the-badge
+[license]: https://github.com/callstackincubator/voltra/blob/main/LICENSE.txt
+[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
diff --git a/packages/voltra/ios/Voltra.podspec b/packages/ios-client/Voltra.podspec
similarity index 69%
rename from packages/voltra/ios/Voltra.podspec
rename to packages/ios-client/Voltra.podspec
index ea42c7ee..4a494e62 100644
--- a/packages/voltra/ios/Voltra.podspec
+++ b/packages/ios-client/Voltra.podspec
@@ -1,6 +1,6 @@
require 'json'
-package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
+package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
Pod::Spec.new do |s|
s.name = 'Voltra'
@@ -16,17 +16,13 @@ Pod::Spec.new do |s|
s.swift_version = '5.9'
s.source = { git: 'https://github.com/callstackincubator/voltra' }
s.static_framework = true
-
- # Swift/Objective-C compatibility
- s.pod_target_xcconfig = {
- 'DEFINES_MODULE' => 'YES',
- }
-
- s.dependency 'ExpoModulesCore'
+ install_modules_dependencies(s)
s.source_files = [
- "app/**/*.swift",
- "ui/**/*.swift",
- "shared/**/*.swift",
+ "ios/app/**/*.swift",
+ "ios/app/**/*.m",
+ "ios/app/**/*.mm",
+ "ios/ui/**/*.swift",
+ "ios/shared/**/*.swift",
]
end
diff --git a/packages/ios-client/expo-plugin/app.plugin.js b/packages/ios-client/expo-plugin/app.plugin.js
new file mode 100644
index 00000000..8cb3b172
--- /dev/null
+++ b/packages/ios-client/expo-plugin/app.plugin.js
@@ -0,0 +1,3 @@
+const plugin = require('./build/cjs/index.js')
+
+module.exports = plugin.default ?? plugin
diff --git a/packages/ios-client/expo-plugin/jest.config.js b/packages/ios-client/expo-plugin/jest.config.js
new file mode 100644
index 00000000..5835d68b
--- /dev/null
+++ b/packages/ios-client/expo-plugin/jest.config.js
@@ -0,0 +1,18 @@
+/** @type {import('jest').Config} */
+module.exports = {
+ testEnvironment: 'node',
+ testMatch: ['/src/**/*.node.test.ts'],
+ modulePathIgnorePatterns: ['/build'],
+ moduleNameMapper: {
+ '^@use-voltra/expo-plugin$': '/../../expo-plugin/src/index.ts',
+ '^@use-voltra/expo-plugin/(.*)$': '/../../expo-plugin/src/$1',
+ },
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ tsconfig: '/tsconfig.jest.json',
+ },
+ ],
+ },
+}
diff --git a/packages/ios-client/expo-plugin/src/constants.ts b/packages/ios-client/expo-plugin/src/constants.ts
new file mode 100644
index 00000000..f9a327e4
--- /dev/null
+++ b/packages/ios-client/expo-plugin/src/constants.ts
@@ -0,0 +1,25 @@
+import type { IOSWidgetFamily } from './types'
+
+export const IOS = {
+ DEPLOYMENT_TARGET: '17.0',
+ SWIFT_VERSION: '5.0',
+ DEVICE_FAMILY: '1,2',
+ LAST_SWIFT_MIGRATION: 1250,
+} as const
+
+/** Default path for user-provided widget images */
+export const DEFAULT_USER_IMAGES_PATH = './assets/voltra'
+
+export const SUPPORTED_IMAGE_EXTENSIONS = /\.(png|jpg|jpeg)$/i
+
+export const DEFAULT_WIDGET_FAMILIES: IOSWidgetFamily[] = ['systemSmall', 'systemMedium', 'systemLarge']
+
+export const WIDGET_FAMILY_MAP: Record = {
+ systemSmall: '.systemSmall',
+ systemMedium: '.systemMedium',
+ systemLarge: '.systemLarge',
+ systemExtraLarge: '.systemExtraLarge',
+ accessoryCircular: '.accessoryCircular',
+ accessoryRectangular: '.accessoryRectangular',
+ accessoryInline: '.accessoryInline',
+}
diff --git a/packages/ios-client/expo-plugin/src/index.ts b/packages/ios-client/expo-plugin/src/index.ts
new file mode 100644
index 00000000..97742c44
--- /dev/null
+++ b/packages/ios-client/expo-plugin/src/index.ts
@@ -0,0 +1,72 @@
+import { IOSConfig } from 'expo/config-plugins'
+
+import { IOS } from './constants'
+import { withIOS, withPushNotifications } from './ios'
+import { withIOS as withIOSWidget } from './ios-widget'
+import type { IOSConfigPluginProps, VoltraIosConfigPlugin } from './types'
+import { ensureURLScheme } from './utils/urlScheme'
+import { validateIOSConfigPluginProps } from './validation'
+
+/**
+ * Voltra iOS Expo config plugin.
+ *
+ * Configures Live Activities, the widget extension, and optional push-to-start support.
+ */
+const withVoltraIos: VoltraIosConfigPlugin = (config, props = {}) => {
+ validateIOSConfigPluginProps(props)
+
+ const iosBundleIdentifier = config.ios?.bundleIdentifier
+ if (!iosBundleIdentifier) {
+ return config
+ }
+
+ const deploymentTarget = props.deploymentTarget || IOS.DEPLOYMENT_TARGET
+ const targetName = props.targetName || `${IOSConfig.XcodeUtils.sanitizedName(config.name)}LiveActivity`
+ const bundleIdentifier = `${iosBundleIdentifier}.${targetName}`
+ const version = config.version || '1.0.0'
+ const buildNumber = config.ios?.buildNumber || '1'
+
+ config = ensureURLScheme(config)
+
+ const hasServerDrivenWidgets = props.widgets?.some((w) => w.serverUpdate) ?? false
+ const keychainGroup =
+ props.keychainGroup ?? (hasServerDrivenWidgets ? `$(AppIdentifierPrefix)${iosBundleIdentifier}` : undefined)
+
+ config = withIOS(config, {
+ groupIdentifier: props.groupIdentifier,
+ widgetIds: props.widgets && props.widgets.length > 0 ? props.widgets.map((w) => w.id) : undefined,
+ widgets: props.widgets,
+ keychainGroup,
+ })
+
+ config = withIOSWidget(config, {
+ targetName,
+ bundleIdentifier,
+ deploymentTarget,
+ widgets: props.widgets,
+ version,
+ buildNumber,
+ ...(props.groupIdentifier ? { groupIdentifier: props.groupIdentifier } : {}),
+ ...(keychainGroup ? { keychainGroup } : {}),
+ ...(props.fonts ? { fonts: props.fonts } : {}),
+ })
+
+ if (props.enablePushNotifications) {
+ config = withPushNotifications(config)
+ }
+
+ return config
+}
+
+export default withVoltraIos
+
+export type {
+ IOSConfigPluginProps,
+ IOSMainAppPluginProps,
+ IOSWidgetConfig,
+ IOSWidgetExtensionFiles,
+ IOSWidgetExtensionPluginProps,
+ IOSWidgetFamily,
+ IOSWidgetServerUpdateConfig,
+ VoltraIosConfigPlugin,
+} from './types'
diff --git a/packages/expo-plugin/src/ios-widget/eas.ts b/packages/ios-client/expo-plugin/src/ios-widget/eas.ts
similarity index 96%
rename from packages/expo-plugin/src/ios-widget/eas.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/eas.ts
index c7b10c68..d62db6ed 100644
--- a/packages/expo-plugin/src/ios-widget/eas.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/eas.ts
@@ -1,6 +1,6 @@
import { ConfigPlugin } from '@expo/config-plugins'
-import { addApplicationGroupsEntitlement } from '../utils/entitlements'
+import { addApplicationGroupsEntitlement } from '@use-voltra/expo-plugin'
import { getWidgetExtensionEntitlements } from './files/entitlements'
export interface ConfigureEasBuildProps {
diff --git a/packages/expo-plugin/src/ios-widget/files/assets.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/assets.ts
similarity index 97%
rename from packages/expo-plugin/src/ios-widget/files/assets.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/assets.ts
index 15bcc282..3fb64daa 100644
--- a/packages/expo-plugin/src/ios-widget/files/assets.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/files/assets.ts
@@ -1,8 +1,9 @@
import * as fs from 'fs'
import * as path from 'path'
-import { DEFAULT_USER_IMAGES_PATH, MAX_IMAGE_SIZE_BYTES, SUPPORTED_IMAGE_EXTENSIONS } from '../../constants'
-import { logger } from '../../utils/logger'
+import { MAX_IMAGE_SIZE_BYTES, logger } from '@use-voltra/expo-plugin'
+
+import { DEFAULT_USER_IMAGES_PATH, SUPPORTED_IMAGE_EXTENSIONS } from '../../constants'
export interface GenerateAssetsOptions {
targetPath: string
diff --git a/packages/expo-plugin/src/ios-widget/files/entitlements.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/entitlements.ts
similarity index 92%
rename from packages/expo-plugin/src/ios-widget/files/entitlements.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/entitlements.ts
index 4cd179b5..ee211786 100644
--- a/packages/expo-plugin/src/ios-widget/files/entitlements.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/files/entitlements.ts
@@ -2,8 +2,8 @@ import plist from '@expo/plist'
import * as fs from 'fs'
import * as path from 'path'
-import { addApplicationGroupsEntitlement } from '../../utils/entitlements'
-import { logger } from '../../utils/logger'
+import { addApplicationGroupsEntitlement } from '@use-voltra/expo-plugin'
+import { logger } from '@use-voltra/expo-plugin'
/**
* Gets the entitlements for the widget extension.
diff --git a/packages/expo-plugin/src/ios-widget/files/index.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/index.ts
similarity index 96%
rename from packages/expo-plugin/src/ios-widget/files/index.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/index.ts
index 33884602..1a59927c 100644
--- a/packages/expo-plugin/src/ios-widget/files/index.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/files/index.ts
@@ -2,7 +2,7 @@ import { ConfigPlugin, withDangerousMod } from '@expo/config-plugins'
import * as fs from 'fs'
import * as path from 'path'
-import type { WidgetConfig } from '../../types'
+import type { IOSWidgetConfig } from '../../types'
import { generateAssets } from './assets'
import { generateEntitlements } from './entitlements'
import { generateInfoPlist } from './infoPlist'
@@ -10,7 +10,7 @@ import { generateSwiftFiles } from './swift'
export interface GenerateWidgetExtensionFilesProps {
targetName: string
- widgets?: WidgetConfig[]
+ widgets?: IOSWidgetConfig[]
groupIdentifier?: string
keychainGroup?: string
version: string
diff --git a/packages/expo-plugin/src/ios-widget/files/infoPlist.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/infoPlist.ts
similarity index 97%
rename from packages/expo-plugin/src/ios-widget/files/infoPlist.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/infoPlist.ts
index 578faf9f..c4ee05f2 100644
--- a/packages/expo-plugin/src/ios-widget/files/infoPlist.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/files/infoPlist.ts
@@ -1,7 +1,7 @@
import * as fs from 'fs'
import * as path from 'path'
-import { logger } from '../../utils/logger'
+import { logger } from '@use-voltra/expo-plugin'
/**
* Generates the Info.plist content for a WidgetKit extension.
diff --git a/packages/expo-plugin/src/ios-widget/files/swift.node.test.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/swift.node.test.ts
similarity index 100%
rename from packages/expo-plugin/src/ios-widget/files/swift.node.test.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/swift.node.test.ts
diff --git a/packages/expo-plugin/src/ios-widget/files/swift.ts b/packages/ios-client/expo-plugin/src/ios-widget/files/swift.ts
similarity index 94%
rename from packages/expo-plugin/src/ios-widget/files/swift.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/files/swift.ts
index cccfad54..2fef4df8 100644
--- a/packages/expo-plugin/src/ios-widget/files/swift.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/files/swift.ts
@@ -2,18 +2,23 @@ import dedent from 'dedent'
import * as fs from 'fs'
import * as path from 'path'
+import {
+ isWidgetLocalizedMap,
+ logger,
+ prerenderWidgetState,
+ widgetLabelEnglish,
+ type PrerenderedWidgetStates,
+ type WidgetLabel,
+} from '@use-voltra/expo-plugin'
+
import { DEFAULT_WIDGET_FAMILIES, WIDGET_FAMILY_MAP } from '../../constants'
-import type { WidgetConfig, WidgetLabel } from '../../types'
+import type { IOSWidgetConfig } from '../../types'
import { VOLTRA_WIDGET_STRINGS_BASENAME } from '../../utils/fileDiscovery'
-import { logger } from '../../utils/logger'
-import type { PrerenderedWidgetStates } from '../../utils/prerender'
-import { prerenderWidgetState } from '../../utils/prerender'
-import { isWidgetLocalizedMap, widgetLabelEnglish } from '../../utils/widgetLabel'
export interface GenerateSwiftFilesOptions {
targetPath: string
projectRoot: string
- widgets?: WidgetConfig[]
+ widgets?: IOSWidgetConfig[]
}
type RenderWidgetToString = (variants: unknown) => string
@@ -32,9 +37,8 @@ type RenderWidgetToString = (variants: unknown) => string
export async function generateSwiftFiles(options: GenerateSwiftFilesOptions): Promise {
const { targetPath, projectRoot, widgets } = options
- // Dynamic import for ESM module compatibility
- // voltra/server is an ESM module, but the plugin is compiled to CommonJS
- const serverModuleId = 'voltra/server'
+ // Dynamic import keeps the plugin CommonJS-compatible while resolving the current package entry.
+ const serverModuleId = '@use-voltra/ios/server'
const { renderWidgetToString } = (await import(serverModuleId)) as {
renderWidgetToString: RenderWidgetToString
}
@@ -142,7 +146,7 @@ function escapeDotStringsValue(s: string): string {
return escapeForSwiftStringLiteral(s)
}
-function collectGalleryStringsByLocale(widgets: WidgetConfig[]): Map> {
+function collectGalleryStringsByLocale(widgets: IOSWidgetConfig[]): Map> {
const byLocale = new Map>()
const add = (locale: string, key: string, value: string) => {
@@ -215,7 +219,7 @@ function clearVoltraWidgetGalleryStringArtifacts(targetPath: string): void {
/**
* Writes `.lproj/VoltraWidgets.strings` for locale-map gallery labels.
*/
-function syncVoltraWidgetGalleryStrings(targetPath: string, widgets: WidgetConfig[] | undefined): void {
+function syncVoltraWidgetGalleryStrings(targetPath: string, widgets: IOSWidgetConfig[] | undefined): void {
const list = widgets ?? []
clearVoltraWidgetGalleryStringArtifacts(targetPath)
@@ -235,7 +239,7 @@ function syncVoltraWidgetGalleryStrings(targetPath: string, widgets: WidgetConfi
}
}
-function widgetUsesGalleryLocalization(widget: WidgetConfig): boolean {
+function widgetUsesGalleryLocalization(widget: IOSWidgetConfig): boolean {
return isWidgetLocalizedMap(widget.displayName) || isWidgetLocalizedMap(widget.description)
}
@@ -261,7 +265,7 @@ function iosWidgetGalleryLabelSwiftExpr(
/**
* Generates Swift code for a single widget struct
*/
-function generateWidgetStruct(widget: WidgetConfig): string {
+function generateWidgetStruct(widget: IOSWidgetConfig): string {
const families = widget.supportedFamilies ?? DEFAULT_WIDGET_FAMILIES
const familiesSwift = families.map((f) => WIDGET_FAMILY_MAP[f]).join(', ')
@@ -299,7 +303,7 @@ function generateWidgetStruct(widget: WidgetConfig): string {
/**
* Generates the VoltraWidgetBundle.swift file content with configured widgets
*/
-function generateWidgetBundleSwift(widgets: WidgetConfig[]): string {
+function generateWidgetBundleSwift(widgets: IOSWidgetConfig[]): string {
// Generate widget structs
const widgetStructs = widgets.map(generateWidgetStruct).join('\n\n')
diff --git a/packages/expo-plugin/src/ios-widget/fonts.ts b/packages/ios-client/expo-plugin/src/ios-widget/fonts.ts
similarity index 97%
rename from packages/expo-plugin/src/ios-widget/fonts.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/fonts.ts
index 63618828..0674d536 100644
--- a/packages/expo-plugin/src/ios-widget/fonts.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/fonts.ts
@@ -13,8 +13,8 @@ import type { ExpoConfig } from 'expo/config'
import { readFileSync, writeFileSync } from 'fs'
import * as path from 'path'
-import { logger } from '../utils/logger'
-import { resolveFontPaths } from '../utils/fonts'
+import { logger } from '@use-voltra/expo-plugin'
+import { resolveFontPaths } from '@use-voltra/expo-plugin'
/**
* Plugin that adds custom fonts to the Live Activity extension.
diff --git a/packages/expo-plugin/src/ios-widget/index.ts b/packages/ios-client/expo-plugin/src/ios-widget/index.ts
similarity index 97%
rename from packages/expo-plugin/src/ios-widget/index.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/index.ts
index 2a92c7bf..f372bdb6 100644
--- a/packages/expo-plugin/src/ios-widget/index.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/index.ts
@@ -1,6 +1,6 @@
import { ConfigPlugin, withPlugins } from '@expo/config-plugins'
-import type { WidgetConfig } from '../types'
+import type { IOSWidgetConfig } from '../types'
import { configureEasBuild } from './eas'
import { generateWidgetExtensionFiles } from './files'
import { withFonts } from './fonts'
@@ -12,7 +12,7 @@ export interface WithIOSProps {
targetName: string
bundleIdentifier: string
deploymentTarget: string
- widgets?: WidgetConfig[]
+ widgets?: IOSWidgetConfig[]
groupIdentifier?: string
keychainGroup?: string
fonts?: string[]
diff --git a/packages/expo-plugin/src/ios-widget/podfile.ts b/packages/ios-client/expo-plugin/src/ios-widget/podfile.ts
similarity index 77%
rename from packages/expo-plugin/src/ios-widget/podfile.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/podfile.ts
index 8f5ab1ca..dd3ff2c0 100644
--- a/packages/expo-plugin/src/ios-widget/podfile.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/podfile.ts
@@ -13,11 +13,9 @@ target '${targetName}' do
require 'pathname'
project_root = "#{Pod::Config.instance.installation_root}/.."
- library_name = 'voltra'
- voltra_module = JSON.parse(\`npx expo-modules-autolinking search -p apple --json --project-root #{project_root}\`)
- podspec_dir_path = File.join(voltra_module[library_name]['path'], 'ios')
- podspec_dir_path = File.realpath(podspec_dir_path) if File.exist?(podspec_dir_path)
- podspec_dir_path = Pathname.new(podspec_dir_path).relative_path_from(Pathname.new(__dir__)).to_path
+ voltra_client_root = \`node --print "require.resolve('@use-voltra/ios-client/package.json', {paths: ['#{project_root}']})" 2>/dev/null\`.strip.gsub('/package.json', '')
+ voltra_widget_podspec_root = File.join(voltra_client_root, 'ios')
+ podspec_dir_path = Pathname.new(voltra_widget_podspec_root).relative_path_from(Pathname.new(__dir__)).to_path
pod 'VoltraWidget', :path => podspec_dir_path
end`
diff --git a/packages/expo-plugin/src/ios-widget/widgetPlist.ts b/packages/ios-client/expo-plugin/src/ios-widget/widgetPlist.ts
similarity index 96%
rename from packages/expo-plugin/src/ios-widget/widgetPlist.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/widgetPlist.ts
index bc1b7033..7494f885 100644
--- a/packages/expo-plugin/src/ios-widget/widgetPlist.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/widgetPlist.ts
@@ -3,13 +3,13 @@ import plist from '@expo/plist'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import { join as joinPath } from 'path'
-import type { WidgetConfig } from '../types'
-import { logger } from '../utils/logger'
+import type { IOSWidgetConfig } from '../types'
+import { logger } from '@use-voltra/expo-plugin'
export interface ConfigureMainAppPlistProps {
targetName: string
groupIdentifier?: string
- widgets?: WidgetConfig[]
+ widgets?: IOSWidgetConfig[]
keychainGroup?: string
}
diff --git a/packages/expo-plugin/src/ios-widget/xcode/buildPhases.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/buildPhases.ts
similarity index 98%
rename from packages/expo-plugin/src/ios-widget/xcode/buildPhases.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/buildPhases.ts
index fc3ab7e1..da554df2 100644
--- a/packages/expo-plugin/src/ios-widget/xcode/buildPhases.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/xcode/buildPhases.ts
@@ -1,7 +1,7 @@
import { XcodeProject } from '@expo/config-plugins'
import * as util from 'util'
-import type { WidgetFiles } from '../../types'
+import type { IOSWidgetExtensionFiles } from '../../types'
const pbxFile = require('xcode/lib/pbxFile')
@@ -14,7 +14,7 @@ export interface AddBuildPhasesOptions {
basename: string
group: string
}
- widgetFiles: WidgetFiles
+ widgetFiles: IOSWidgetExtensionFiles
}
export interface EnsureBuildPhasesOptions extends AddBuildPhasesOptions {
diff --git a/packages/expo-plugin/src/ios-widget/xcode/configurationList.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/configurationList.ts
similarity index 100%
rename from packages/expo-plugin/src/ios-widget/xcode/configurationList.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/configurationList.ts
diff --git a/packages/expo-plugin/src/ios-widget/xcode/groups.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/groups.ts
similarity index 96%
rename from packages/expo-plugin/src/ios-widget/xcode/groups.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/groups.ts
index 8fae66d4..f5f76a24 100644
--- a/packages/expo-plugin/src/ios-widget/xcode/groups.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/xcode/groups.ts
@@ -1,12 +1,12 @@
import { XcodeProject } from '@expo/config-plugins'
-import type { WidgetFiles } from '../../types'
+import type { IOSWidgetExtensionFiles } from '../../types'
const pbxFile = require('xcode/lib/pbxFile')
export interface AddPbxGroupOptions {
targetName: string
- widgetFiles: WidgetFiles
+ widgetFiles: IOSWidgetExtensionFiles
}
/**
diff --git a/packages/expo-plugin/src/ios-widget/xcode/index.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/index.ts
similarity index 97%
rename from packages/expo-plugin/src/ios-widget/xcode/index.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/index.ts
index 6b6e5ced..48f47fb6 100644
--- a/packages/expo-plugin/src/ios-widget/xcode/index.ts
+++ b/packages/ios-client/expo-plugin/src/ios-widget/xcode/index.ts
@@ -1,7 +1,7 @@
import { ConfigPlugin, withXcodeProject } from '@expo/config-plugins'
import * as path from 'path'
-import { getWidgetFiles } from '../../utils/fileDiscovery'
+import { getIOSWidgetExtensionFiles } from '../../utils/fileDiscovery'
import { addBuildPhases, ensureBuildPhases } from './buildPhases'
import { addXCConfigurationList, ensureXCConfigurationList } from './configurationList'
import { addPbxGroup, ensurePbxGroup } from './groups'
@@ -45,7 +45,7 @@ export const configureXcodeProject: ConfigPlugin = (
const { platformProjectRoot } = config.modRequest
const targetPath = path.join(platformProjectRoot, targetName)
- const widgetFiles = getWidgetFiles(targetPath, targetName)
+ const widgetFiles = getIOSWidgetExtensionFiles(targetPath, targetName)
// Read main app target settings to synchronize code signing
const mainAppSettings = getMainAppTargetSettings(xcodeProject)
diff --git a/packages/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts
similarity index 100%
rename from packages/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts
diff --git a/packages/expo-plugin/src/ios-widget/xcode/productFile.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/productFile.ts
similarity index 100%
rename from packages/expo-plugin/src/ios-widget/xcode/productFile.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/productFile.ts
diff --git a/packages/expo-plugin/src/ios-widget/xcode/target.ts b/packages/ios-client/expo-plugin/src/ios-widget/xcode/target.ts
similarity index 100%
rename from packages/expo-plugin/src/ios-widget/xcode/target.ts
rename to packages/ios-client/expo-plugin/src/ios-widget/xcode/target.ts
diff --git a/packages/expo-plugin/src/ios/eas.ts b/packages/ios-client/expo-plugin/src/ios/eas.ts
similarity index 91%
rename from packages/expo-plugin/src/ios/eas.ts
rename to packages/ios-client/expo-plugin/src/ios/eas.ts
index eb8f20c4..c00b022a 100644
--- a/packages/expo-plugin/src/ios/eas.ts
+++ b/packages/ios-client/expo-plugin/src/ios/eas.ts
@@ -1,6 +1,6 @@
import { ConfigPlugin } from '@expo/config-plugins'
-import { addApplicationGroupsEntitlement } from '../utils/entitlements'
+import { addApplicationGroupsEntitlement } from '@use-voltra/expo-plugin'
export interface ConfigureEasProps {
groupIdentifier?: string
diff --git a/packages/expo-plugin/src/ios/entitlements.ts b/packages/ios-client/expo-plugin/src/ios/entitlements.ts
similarity index 95%
rename from packages/expo-plugin/src/ios/entitlements.ts
rename to packages/ios-client/expo-plugin/src/ios/entitlements.ts
index 3485d91d..09c4a746 100644
--- a/packages/expo-plugin/src/ios/entitlements.ts
+++ b/packages/ios-client/expo-plugin/src/ios/entitlements.ts
@@ -1,6 +1,6 @@
import { ConfigPlugin } from '@expo/config-plugins'
-import { addApplicationGroupsEntitlement } from '../utils/entitlements'
+import { addApplicationGroupsEntitlement } from '@use-voltra/expo-plugin'
export interface ConfigureEntitlementsProps {
groupIdentifier?: string
diff --git a/packages/expo-plugin/src/ios/index.ts b/packages/ios-client/expo-plugin/src/ios/index.ts
similarity index 96%
rename from packages/expo-plugin/src/ios/index.ts
rename to packages/ios-client/expo-plugin/src/ios/index.ts
index 0e8c732b..f0ed6f14 100644
--- a/packages/expo-plugin/src/ios/index.ts
+++ b/packages/ios-client/expo-plugin/src/ios/index.ts
@@ -7,7 +7,7 @@ import { configureInfoPlist } from './infoPlist'
export interface IOSConfigProps {
groupIdentifier?: string
widgetIds?: string[]
- widgets?: import('../types').WidgetConfig[]
+ widgets?: import('../types').IOSWidgetConfig[]
keychainGroup?: string
}
diff --git a/packages/expo-plugin/src/ios/infoPlist.ts b/packages/ios-client/expo-plugin/src/ios/infoPlist.ts
similarity index 96%
rename from packages/expo-plugin/src/ios/infoPlist.ts
rename to packages/ios-client/expo-plugin/src/ios/infoPlist.ts
index 5b99422b..8a3c1173 100644
--- a/packages/expo-plugin/src/ios/infoPlist.ts
+++ b/packages/ios-client/expo-plugin/src/ios/infoPlist.ts
@@ -1,11 +1,11 @@
import { ConfigPlugin, withInfoPlist } from '@expo/config-plugins'
-import type { WidgetConfig } from '../types'
+import type { IOSWidgetConfig } from '../types'
export interface ConfigureInfoPlistProps {
groupIdentifier?: string
widgetIds?: string[]
- widgets?: WidgetConfig[]
+ widgets?: IOSWidgetConfig[]
keychainGroup?: string
}
diff --git a/packages/expo-plugin/src/ios/pushNotifications.ts b/packages/ios-client/expo-plugin/src/ios/pushNotifications.ts
similarity index 100%
rename from packages/expo-plugin/src/ios/pushNotifications.ts
rename to packages/ios-client/expo-plugin/src/ios/pushNotifications.ts
diff --git a/packages/ios-client/expo-plugin/src/types.ts b/packages/ios-client/expo-plugin/src/types.ts
new file mode 100644
index 00000000..26f85ce3
--- /dev/null
+++ b/packages/ios-client/expo-plugin/src/types.ts
@@ -0,0 +1,89 @@
+import type { ConfigPlugin } from '@expo/config-plugins'
+
+import type { WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin'
+
+/**
+ * Supported iOS Home Screen widget size families.
+ */
+export type IOSWidgetFamily =
+ | 'systemSmall'
+ | 'systemMedium'
+ | 'systemLarge'
+ | 'systemExtraLarge'
+ | 'accessoryCircular'
+ | 'accessoryRectangular'
+ | 'accessoryInline'
+
+/**
+ * Configuration for a single iOS home screen widget.
+ */
+export interface IOSWidgetConfig {
+ /**
+ * Unique identifier for the widget (used as the widget kind and in JS API)
+ */
+ id: string
+ displayName: WidgetLabel
+ description: WidgetLabel
+ /** @default ['systemSmall', 'systemMedium', 'systemLarge'] */
+ supportedFamilies?: IOSWidgetFamily[]
+ initialStatePath?: WidgetInitialStatePath
+ serverUpdate?: IOSWidgetServerUpdateConfig
+}
+
+/**
+ * Server-driven iOS widget updates (WidgetKit background refresh).
+ */
+export interface IOSWidgetServerUpdateConfig {
+ url: string
+ /** @default 15 */
+ intervalMinutes?: number
+ /** @default false */
+ refresh?: boolean
+}
+
+/**
+ * Files in the widget extension target (Xcode build phases / groups).
+ */
+export interface IOSWidgetExtensionFiles {
+ swiftFiles: string[]
+ entitlementFiles: string[]
+ plistFiles: string[]
+ assetDirectories: string[]
+ intentFiles: string[]
+ /** Paths relative to the widget extension root (e.g. en.lproj/VoltraWidgets.strings) */
+ localizedStringResources: string[]
+}
+
+/**
+ * Options for `@use-voltra/ios-client` Expo config plugin.
+ */
+export interface IOSConfigPluginProps {
+ enablePushNotifications?: boolean
+ groupIdentifier?: string
+ widgets?: IOSWidgetConfig[]
+ deploymentTarget?: string
+ targetName?: string
+ fonts?: string[]
+ keychainGroup?: string
+}
+
+export type VoltraIosConfigPlugin = ConfigPlugin
+
+export interface IOSMainAppPluginProps {
+ groupIdentifier?: string
+ widgetIds?: string[]
+ widgets?: IOSWidgetConfig[]
+ keychainGroup?: string
+}
+
+export interface IOSWidgetExtensionPluginProps {
+ targetName: string
+ bundleIdentifier: string
+ deploymentTarget: string
+ widgets?: IOSWidgetConfig[]
+ groupIdentifier?: string
+ keychainGroup?: string
+ fonts?: string[]
+ version: string
+ buildNumber: string
+}
diff --git a/packages/expo-plugin/src/utils/fileDiscovery.ts b/packages/ios-client/expo-plugin/src/utils/fileDiscovery.ts
similarity index 91%
rename from packages/expo-plugin/src/utils/fileDiscovery.ts
rename to packages/ios-client/expo-plugin/src/utils/fileDiscovery.ts
index 4da07365..b25ad50a 100644
--- a/packages/expo-plugin/src/utils/fileDiscovery.ts
+++ b/packages/ios-client/expo-plugin/src/utils/fileDiscovery.ts
@@ -1,8 +1,8 @@
import * as fs from 'fs'
import * as path from 'path'
-import type { WidgetFiles } from '../types'
-import { logger } from './logger'
+import type { IOSWidgetExtensionFiles } from '../types'
+import { logger } from '@use-voltra/expo-plugin'
/** Table name matches basename without extension; co-located per locale in *.lproj */
export const VOLTRA_WIDGET_STRINGS_BASENAME = 'VoltraWidgets.strings'
@@ -17,8 +17,8 @@ export const VOLTRA_WIDGET_STRINGS_BASENAME = 'VoltraWidgets.strings'
* @param targetName - Name of the target (for entitlements file)
* @returns Categorized lists of files for Xcode project configuration
*/
-export function getWidgetFiles(targetPath: string, targetName: string): WidgetFiles {
- const widgetFiles: WidgetFiles = {
+export function getIOSWidgetExtensionFiles(targetPath: string, targetName: string): IOSWidgetExtensionFiles {
+ const widgetFiles: IOSWidgetExtensionFiles = {
swiftFiles: [],
entitlementFiles: [],
plistFiles: [],
diff --git a/packages/expo-plugin/src/utils/urlScheme.ts b/packages/ios-client/expo-plugin/src/utils/urlScheme.ts
similarity index 100%
rename from packages/expo-plugin/src/utils/urlScheme.ts
rename to packages/ios-client/expo-plugin/src/utils/urlScheme.ts
diff --git a/packages/ios-client/expo-plugin/src/validation.node.test.ts b/packages/ios-client/expo-plugin/src/validation.node.test.ts
new file mode 100644
index 00000000..bd456a16
--- /dev/null
+++ b/packages/ios-client/expo-plugin/src/validation.node.test.ts
@@ -0,0 +1,19 @@
+import { validateIOSConfigPluginProps } from './validation'
+
+describe('validateIOSConfigPluginProps', () => {
+ it('accepts valid groupIdentifier', () => {
+ expect(() =>
+ validateIOSConfigPluginProps({
+ groupIdentifier: 'group.com.example.app',
+ })
+ ).not.toThrow()
+ })
+
+ it('rejects groupIdentifier without group. prefix', () => {
+ expect(() =>
+ validateIOSConfigPluginProps({
+ groupIdentifier: 'com.example.app',
+ })
+ ).toThrow(/must start with 'group.'/)
+ })
+})
diff --git a/packages/ios-client/expo-plugin/src/validation.ts b/packages/ios-client/expo-plugin/src/validation.ts
new file mode 100644
index 00000000..0f30f5aa
--- /dev/null
+++ b/packages/ios-client/expo-plugin/src/validation.ts
@@ -0,0 +1,63 @@
+import { validateHomeScreenWidgetId, validateInitialStatePath, validateWidgetLabel } from '@use-voltra/expo-plugin'
+
+import type { IOSConfigPluginProps, IOSWidgetConfig, IOSWidgetFamily } from './types'
+
+const VALID_FAMILIES: Set = new Set([
+ 'systemSmall',
+ 'systemMedium',
+ 'systemLarge',
+ 'systemExtraLarge',
+ 'accessoryCircular',
+ 'accessoryRectangular',
+ 'accessoryInline',
+])
+
+export function validateIOSWidgetConfig(widget: IOSWidgetConfig): void {
+ validateHomeScreenWidgetId(widget.id)
+ validateWidgetLabel(widget.displayName, widget.id, 'displayName')
+ validateWidgetLabel(widget.description, widget.id, 'description')
+ validateInitialStatePath(widget.initialStatePath, widget.id)
+
+ if (widget.supportedFamilies) {
+ if (!Array.isArray(widget.supportedFamilies)) {
+ throw new Error(`Widget '${widget.id}': supportedFamilies must be an array`)
+ }
+
+ for (const family of widget.supportedFamilies) {
+ if (!VALID_FAMILIES.has(family)) {
+ throw new Error(
+ `Widget '${widget.id}': Invalid widget family '${family}'. ` +
+ `Valid families are: ${Array.from(VALID_FAMILIES).join(', ')}`
+ )
+ }
+ }
+ }
+}
+
+export function validateIOSConfigPluginProps(props: IOSConfigPluginProps): void {
+ if (props.groupIdentifier !== undefined) {
+ if (typeof props.groupIdentifier !== 'string') {
+ throw new Error('groupIdentifier must be a string')
+ }
+
+ if (!props.groupIdentifier.startsWith('group.')) {
+ throw new Error(`groupIdentifier '${props.groupIdentifier}' must start with 'group.'`)
+ }
+ }
+
+ if (props.widgets !== undefined) {
+ if (!Array.isArray(props.widgets)) {
+ throw new Error('widgets must be an array')
+ }
+
+ const seenIds = new Set()
+ for (const widget of props.widgets) {
+ validateIOSWidgetConfig(widget)
+
+ if (seenIds.has(widget.id)) {
+ throw new Error(`Duplicate widget ID: '${widget.id}'`)
+ }
+ seenIds.add(widget.id)
+ }
+ }
+}
diff --git a/packages/ios-client/expo-plugin/tsconfig.base.json b/packages/ios-client/expo-plugin/tsconfig.base.json
new file mode 100644
index 00000000..6bb57ecd
--- /dev/null
+++ b/packages/ios-client/expo-plugin/tsconfig.base.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020"],
+ "rootDir": "./src",
+ "moduleResolution": "node",
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "include": ["./src"],
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*", "**/*.node.test.ts"]
+}
diff --git a/packages/ios-client/tsconfig.cjs.json b/packages/ios-client/expo-plugin/tsconfig.cjs.json
similarity index 100%
rename from packages/ios-client/tsconfig.cjs.json
rename to packages/ios-client/expo-plugin/tsconfig.cjs.json
diff --git a/packages/ios-client/tsconfig.esm.json b/packages/ios-client/expo-plugin/tsconfig.esm.json
similarity index 100%
rename from packages/ios-client/tsconfig.esm.json
rename to packages/ios-client/expo-plugin/tsconfig.esm.json
diff --git a/packages/ios-client/expo-plugin/tsconfig.jest.json b/packages/ios-client/expo-plugin/tsconfig.jest.json
new file mode 100644
index 00000000..c0c766d0
--- /dev/null
+++ b/packages/ios-client/expo-plugin/tsconfig.jest.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "types": ["jest", "node"]
+ },
+ "include": ["./src/**/*.ts"]
+}
diff --git a/packages/ios-client/expo-plugin/tsconfig.typecheck.json b/packages/ios-client/expo-plugin/tsconfig.typecheck.json
new file mode 100644
index 00000000..58e6f899
--- /dev/null
+++ b/packages/ios-client/expo-plugin/tsconfig.typecheck.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "module": "ES2020",
+ "rootDir": "../../..",
+ "baseUrl": "../../..",
+ "paths": {
+ "@use-voltra/expo-plugin": ["packages/expo-plugin/src/index.ts"]
+ }
+ },
+ "include": ["./src"]
+}
diff --git a/packages/ios-client/tsconfig.types.json b/packages/ios-client/expo-plugin/tsconfig.types.json
similarity index 100%
rename from packages/ios-client/tsconfig.types.json
rename to packages/ios-client/expo-plugin/tsconfig.types.json
diff --git a/packages/voltra/ios/Package.swift b/packages/ios-client/ios/Package.swift
similarity index 100%
rename from packages/voltra/ios/Package.swift
rename to packages/ios-client/ios/Package.swift
diff --git a/packages/voltra/ios/tests/JSGradientParserTests.swift b/packages/ios-client/ios/Tests/JSGradientParserTests.swift
similarity index 100%
rename from packages/voltra/ios/tests/JSGradientParserTests.swift
rename to packages/ios-client/ios/Tests/JSGradientParserTests.swift
diff --git a/packages/voltra/ios/Tests/VoltraSharedTests/SmokeTests.swift b/packages/ios-client/ios/Tests/VoltraSharedTests/SmokeTests.swift
similarity index 100%
rename from packages/voltra/ios/Tests/VoltraSharedTests/SmokeTests.swift
rename to packages/ios-client/ios/Tests/VoltraSharedTests/SmokeTests.swift
diff --git a/packages/voltra/ios/VoltraWidget.podspec b/packages/ios-client/ios/VoltraWidget.podspec
similarity index 100%
rename from packages/voltra/ios/VoltraWidget.podspec
rename to packages/ios-client/ios/VoltraWidget.podspec
diff --git a/packages/ios-client/ios/app/NativeVoltra.h b/packages/ios-client/ios/app/NativeVoltra.h
new file mode 100644
index 00000000..4135887f
--- /dev/null
+++ b/packages/ios-client/ios/app/NativeVoltra.h
@@ -0,0 +1,5 @@
+#import
+
+@interface NativeVoltra : NativeVoltraSpecBase
+
+@end
diff --git a/packages/ios-client/ios/app/NativeVoltra.mm b/packages/ios-client/ios/app/NativeVoltra.mm
new file mode 100644
index 00000000..4288eda5
--- /dev/null
+++ b/packages/ios-client/ios/app/NativeVoltra.mm
@@ -0,0 +1,211 @@
+#import "NativeVoltra.h"
+
+#if __has_include("Voltra/Voltra-Swift.h")
+#import "Voltra/Voltra-Swift.h"
+#else
+#import "Voltra-Swift.h"
+#endif
+
+@interface NativeVoltra () {
+ VoltraModule *_module;
+}
+@end
+
+@implementation NativeVoltra
+
+- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *_Nonnull)eventEmitterCallbackWrapper
+{
+ [super setEventEmitterCallback:eventEmitterCallbackWrapper];
+
+ [self.module startMonitoringWithEventHandler:^(NSString *eventName, NSDictionary *eventData) {
+ if ([eventName isEqualToString:@"interaction"]) {
+ [self emitOnInteraction:eventData];
+ } else if ([eventName isEqualToString:@"stateChange"]) {
+ [self emitOnStateChanged:eventData];
+ } else if ([eventName isEqualToString:@"activityTokenReceived"]) {
+ [self emitOnActivityTokenReceived:eventData];
+ } else if ([eventName isEqualToString:@"activityPushToStartTokenReceived"]) {
+ [self emitOnActivityPushToStartTokenReceived:eventData];
+ }
+ }];
+}
+
+- (VoltraModule *)module
+{
+ if (!_module) {
+ _module = [VoltraModule new];
+ }
+ return _module;
+}
+
+- (std::shared_ptr)getTurboModule:
+ (const facebook::react::ObjCTurboModule::InitParams &)params
+{
+ return std::make_shared(params);
+}
+
++ (NSString *)moduleName
+{
+ return @"NativeVoltra";
+}
+
+#pragma mark - NativeVoltraSpec
+
+- (void)startLiveActivity:(NSString *)jsonString
+ options:(JS::NativeVoltra::StartVoltraOptions &)options
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ StartVoltraOptions *opts = [StartVoltraOptions new];
+ opts.activityName = options.activityName();
+ opts.deepLinkUrl = options.deepLinkUrl();
+ opts.channelId = options.channelId();
+ if (auto v = options.staleDate()) opts.staleDate = @(v.value());
+ if (auto v = options.relevanceScore()) opts.relevanceScore = @(v.value());
+ [self.module startLiveActivity:jsonString options:opts completion:^(NSString *activityId, NSError *error) {
+ if (error) { reject(@"startLiveActivity", error.localizedDescription, error); } else { resolve(activityId); }
+ }];
+}
+
+- (void)updateLiveActivity:(NSString *)activityId
+ jsonString:(NSString *)jsonString
+ options:(JS::NativeVoltra::UpdateVoltraOptions &)options
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ UpdateVoltraOptions *opts = [UpdateVoltraOptions new];
+ if (auto v = options.staleDate()) opts.staleDate = @(v.value());
+ if (auto v = options.relevanceScore()) opts.relevanceScore = @(v.value());
+ [self.module updateLiveActivity:activityId jsonString:jsonString options:opts completion:^(NSError *error) {
+ if (error) { reject(@"updateLiveActivity", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)endLiveActivity:(NSString *)activityId
+ options:(JS::NativeVoltra::EndVoltraOptions &)options
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ EndVoltraOptions *opts = [EndVoltraOptions new];
+ if (auto dismissal = options.dismissalPolicy()) {
+ DismissalPolicyOptions *policy = [DismissalPolicyOptions new];
+ policy.type = dismissal->type();
+ if (auto date = dismissal->date()) policy.date = @(date.value());
+ opts.dismissalPolicy = policy;
+ }
+ [self.module endLiveActivity:activityId options:opts completion:^(NSError *error) {
+ if (error) { reject(@"endLiveActivity", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)endAllLiveActivities:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module endAllLiveActivities:^(NSError *error) {
+ if (error) { reject(@"endAllLiveActivities", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)getLatestVoltraActivityId:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ resolve([self.module getLatestVoltraActivityId]);
+}
+
+- (void)listVoltraActivityIds:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ resolve([self.module listVoltraActivityIds]);
+}
+
+- (NSNumber *)isLiveActivityActive:(NSString *)activityName
+{
+ return @([self.module isLiveActivityActive:activityName]);
+}
+
+- (NSNumber *)isHeadless
+{
+ return @([self.module isHeadless]);
+}
+
+- (void)preloadImages:(NSArray *)images resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module preloadImages:images completion:^(NSDictionary *result, NSError *error) {
+ if (error) { reject(@"preloadImages", error.localizedDescription, error); } else { resolve(result); }
+ }];
+}
+
+- (void)reloadLiveActivities:(NSArray *)activityNames resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module reloadLiveActivities:activityNames completion:^(NSError *error) {
+ if (error) { reject(@"reloadLiveActivities", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)clearPreloadedImages:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module clearPreloadedImages:keys completion:^{ resolve(nil); }];
+}
+
+- (void)updateWidget:(NSString *)widgetId
+ jsonString:(NSString *)jsonString
+ options:(JS::NativeVoltra::UpdateWidgetOptions &)options
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ UpdateWidgetOptions *opts = [UpdateWidgetOptions new];
+ opts.deepLinkUrl = options.deepLinkUrl();
+ [self.module updateWidget:widgetId jsonString:jsonString options:opts completion:^(NSError *error) {
+ if (error) { reject(@"updateWidget", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)scheduleWidget:(NSString *)widgetId
+ timelineJson:(NSString *)timelineJson
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module scheduleWidget:widgetId timelineJson:timelineJson completion:^(NSError *error) {
+ if (error) { reject(@"scheduleWidget", error.localizedDescription, error); } else { resolve(nil); }
+ }];
+}
+
+- (void)reloadWidgets:(NSArray *)widgetIds resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module reloadWidgets:widgetIds completion:^{ resolve(nil); }];
+}
+
+- (void)clearWidget:(NSString *)widgetId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module clearWidget:widgetId completion:^{ resolve(nil); }];
+}
+
+- (void)clearAllWidgets:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module clearAllWidgets:^{ resolve(nil); }];
+}
+
+- (void)getActiveWidgets:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module getActiveWidgets:^(NSArray *result, NSError *error) {
+ if (error) { reject(@"getActiveWidgets", error.localizedDescription, error); } else { resolve(result); }
+ }];
+}
+
+- (void)setWidgetServerCredentials:(JS::NativeVoltra::WidgetServerCredentials &)credentials
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module setWidgetServerCredentials:credentials.token() headers:(NSDictionary *)credentials.headers()];
+ resolve(nil);
+}
+
+- (void)clearWidgetServerCredentials:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
+{
+ [self.module clearWidgetServerCredentials];
+ resolve(nil);
+}
+
+- (void)dealloc
+{
+ [self.module stopMonitoring];
+}
+
+@end
diff --git a/packages/voltra/ios/app/VoltraActivity.swift b/packages/ios-client/ios/app/VoltraActivity.swift
similarity index 100%
rename from packages/voltra/ios/app/VoltraActivity.swift
rename to packages/ios-client/ios/app/VoltraActivity.swift
diff --git a/packages/voltra/ios/app/VoltraImagePreload.swift b/packages/ios-client/ios/app/VoltraImagePreload.swift
similarity index 86%
rename from packages/voltra/ios/app/VoltraImagePreload.swift
rename to packages/ios-client/ios/app/VoltraImagePreload.swift
index fb3eedd6..467fa4a1 100644
--- a/packages/voltra/ios/app/VoltraImagePreload.swift
+++ b/packages/ios-client/ios/app/VoltraImagePreload.swift
@@ -1,61 +1,49 @@
-import ExpoModulesCore
import Foundation
import os
import UIKit
// MARK: - Types
-/// Options for preloading a single image
-public struct PreloadImageOptions: Record {
- /// The URL to download the image from
- @Field
+public struct PreloadImageOptions {
public var url: String
-
- /// The key to use when referencing this image (used as assetName)
- @Field
public var key: String
-
- /// HTTP method to use (GET, POST, PUT). Defaults to GET.
- @Field
public var method: String?
-
- /// Optional HTTP headers to include in the request
- @Field
public var headers: [String: String]?
- public init() {}
+ public init(_ dict: NSDictionary) {
+ url = dict["url"] as? String ?? ""
+ key = dict["key"] as? String ?? ""
+ method = dict["method"] as? String
+ headers = dict["headers"] as? [String: String]
+ }
}
-/// Result of a failed image preload
-public struct PreloadImageFailure: Record {
- @Field
+public struct PreloadImageFailure {
public var key: String
-
- @Field
public var error: String
- public init() {}
-
public init(key: String, error: String) {
self.key = key
self.error = error
}
+
+ public func toDictionary() -> NSDictionary {
+ ["key": key, "error": error]
+ }
}
-/// Result of preloading images
-public struct PreloadImagesResult: Record {
- @Field
+public struct PreloadImagesResult {
public var succeeded: [String]
-
- @Field
public var failed: [PreloadImageFailure]
- public init() {}
-
public init(succeeded: [String], failed: [PreloadImageFailure]) {
self.succeeded = succeeded
self.failed = failed
}
+
+ public func toDictionary() -> NSDictionary {
+ ["succeeded": succeeded, "failed": failed.map { $0.toDictionary() }]
+ }
}
// MARK: - Errors
diff --git a/packages/voltra/ios/app/VoltraLiveActivityManager.swift b/packages/ios-client/ios/app/VoltraLiveActivityManager.swift
similarity index 100%
rename from packages/voltra/ios/app/VoltraLiveActivityManager.swift
rename to packages/ios-client/ios/app/VoltraLiveActivityManager.swift
diff --git a/packages/voltra/ios/app/VoltraLiveActivityService.swift b/packages/ios-client/ios/app/VoltraLiveActivityService.swift
similarity index 100%
rename from packages/voltra/ios/app/VoltraLiveActivityService.swift
rename to packages/ios-client/ios/app/VoltraLiveActivityService.swift
diff --git a/packages/ios-client/ios/app/VoltraModule.swift b/packages/ios-client/ios/app/VoltraModule.swift
new file mode 100644
index 00000000..68d8e0a3
--- /dev/null
+++ b/packages/ios-client/ios/app/VoltraModule.swift
@@ -0,0 +1,215 @@
+import Foundation
+
+public enum VoltraErrors: Error {
+ case unsupportedOS
+ case notFound
+ case liveActivitiesNotEnabled
+ case unexpectedError(Error)
+}
+
+@objc public final class VoltraModule: NSObject {
+ private let impl: VoltraModuleImpl
+
+ @objc override public init() {
+ impl = VoltraModuleImpl()
+ super.init()
+ }
+
+ @objc public func startMonitoringWithEventHandler(
+ _ handler: @escaping (NSString, NSDictionary) -> Void
+ ) {
+ impl.startMonitoring { eventName, eventData in
+ handler(eventName as NSString, eventData as NSDictionary)
+ }
+ }
+
+ @objc public func stopMonitoring() {
+ impl.stopMonitoring()
+ }
+
+ // MARK: - Live Activity
+
+ @objc public func startLiveActivity(
+ _ jsonString: String,
+ options: StartVoltraOptions?,
+ completion: @escaping (String?, Error?) -> Void
+ ) {
+ Task {
+ do {
+ try completion(await impl.startLiveActivity(jsonString: jsonString, options: options), nil)
+ } catch {
+ completion(nil, error)
+ }
+ }
+ }
+
+ @objc public func updateLiveActivity(
+ _ activityId: String,
+ jsonString: String,
+ options: UpdateVoltraOptions?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ Task {
+ do {
+ try await impl.updateLiveActivity(activityId: activityId, jsonString: jsonString, options: options)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func endLiveActivity(
+ _ activityId: String,
+ options: EndVoltraOptions?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ Task {
+ do {
+ try await impl.endLiveActivity(activityId: activityId, options: options)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func endAllLiveActivities(_ completion: @escaping (Error?) -> Void) {
+ Task {
+ do {
+ try await impl.endAllLiveActivities()
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func getLatestVoltraActivityId() -> String? {
+ impl.getLatestVoltraActivityId()
+ }
+
+ @objc public func listVoltraActivityIds() -> [String] {
+ impl.listVoltraActivityIds()
+ }
+
+ @objc public func isLiveActivityActive(_ activityName: String) -> Bool {
+ impl.isLiveActivityActive(name: activityName)
+ }
+
+ @objc public func isHeadless() -> Bool {
+ impl.isHeadless()
+ }
+
+ // MARK: - Images
+
+ @objc public func preloadImages(
+ _ images: NSArray,
+ completion: @escaping (NSDictionary?, Error?) -> Void
+ ) {
+ let opts = images.compactMap { $0 as? NSDictionary }.map(PreloadImageOptions.init)
+ Task {
+ do {
+ try completion(await impl.preloadImages(images: opts).toDictionary(), nil)
+ } catch {
+ completion(nil, error)
+ }
+ }
+ }
+
+ @objc public func reloadLiveActivities(
+ _ activityNames: NSArray?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ let names = activityNames?.compactMap { $0 as? String }
+ Task {
+ do {
+ try await impl.reloadLiveActivities(activityNames: names)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func clearPreloadedImages(_ keys: NSArray?, completion: @escaping () -> Void) {
+ Task {
+ await impl.clearPreloadedImages(keys: keys?.compactMap { $0 as? String })
+ completion()
+ }
+ }
+
+ // MARK: - Home Screen Widgets
+
+ @objc public func updateWidget(
+ _ widgetId: String,
+ jsonString: String,
+ options: UpdateWidgetOptions?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ Task {
+ do {
+ try await impl.updateWidget(widgetId: widgetId, jsonString: jsonString, options: options)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func scheduleWidget(
+ _ widgetId: String,
+ timelineJson: String,
+ completion: @escaping (Error?) -> Void
+ ) {
+ Task {
+ do {
+ try await impl.scheduleWidget(widgetId: widgetId, timelineJson: timelineJson)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ @objc public func reloadWidgets(_ widgetIds: NSArray?, completion: @escaping () -> Void) {
+ Task {
+ await impl.reloadWidgets(widgetIds: widgetIds?.compactMap { $0 as? String })
+ completion()
+ }
+ }
+
+ @objc public func clearWidget(_ widgetId: String, completion: @escaping () -> Void) {
+ Task {
+ await impl.clearWidget(widgetId: widgetId)
+ completion()
+ }
+ }
+
+ @objc public func clearAllWidgets(_ completion: @escaping () -> Void) {
+ Task {
+ await impl.clearAllWidgets()
+ completion()
+ }
+ }
+
+ @objc public func getActiveWidgets(_ completion: @escaping (NSArray?, Error?) -> Void) {
+ Task {
+ do {
+ try completion(await impl.getActiveWidgets() as NSArray, nil)
+ } catch {
+ completion(nil, error)
+ }
+ }
+ }
+
+ // MARK: - Widget Server Credentials
+
+ @objc public func setWidgetServerCredentials(_ token: String, headers: NSDictionary?) {
+ impl.setWidgetServerCredentials(token: token, headers: headers as? [String: String])
+ }
+
+ @objc public func clearWidgetServerCredentials() {
+ impl.clearWidgetServerCredentials()
+ }
+}
diff --git a/packages/voltra/ios/app/VoltraModuleImpl.swift b/packages/ios-client/ios/app/VoltraModuleImpl.swift
similarity index 87%
rename from packages/voltra/ios/app/VoltraModuleImpl.swift
rename to packages/ios-client/ios/app/VoltraModuleImpl.swift
index 16a7cc65..c7f45e4c 100644
--- a/packages/voltra/ios/app/VoltraModuleImpl.swift
+++ b/packages/ios-client/ios/app/VoltraModuleImpl.swift
@@ -1,8 +1,8 @@
import ActivityKit
import Compression
-import ExpoModulesCore
import Foundation
import os
+import UIKit
/// Implementation details for VoltraModule to keep the main module file clean
public class VoltraModuleImpl {
@@ -30,8 +30,8 @@ public class VoltraModuleImpl {
// MARK: - Lifecycle & Monitoring
- func startMonitoring() {
- // Note: Event bus subscription is handled by VoltraModule since it has access to sendEvent()
+ func startMonitoring(handler: @escaping (String, [String: Any]) -> Void) {
+ VoltraEventBus.shared.subscribe(handler: handler)
liveActivityService.startMonitoring(enablePush: pushNotificationsEnabled)
}
@@ -43,9 +43,9 @@ public class VoltraModuleImpl {
// MARK: - Live Activities
func startLiveActivity(jsonString: String, options: StartVoltraOptions?) async throws -> String {
- guard #available(iOS 16.2, *) else { throw VoltraModule.VoltraErrors.unsupportedOS }
+ guard #available(iOS 16.2, *) else { throw VoltraErrors.unsupportedOS }
guard VoltraLiveActivityService.areActivitiesEnabled() else {
- throw VoltraModule.VoltraErrors.liveActivitiesNotEnabled
+ throw VoltraErrors.liveActivitiesNotEnabled
}
do {
@@ -57,12 +57,12 @@ public class VoltraModuleImpl {
// Extract staleDate and relevanceScore from options
let staleDate: Date? = {
- if let staleDateMs = options?.staleDate {
+ if let staleDateMs = options?.staleDate?.doubleValue {
return Date(timeIntervalSince1970: staleDateMs / 1000.0)
}
return nil
}()
- let relevanceScore: Double = options?.relevanceScore ?? 0.0
+ let relevanceScore: Double = options?.relevanceScore?.doubleValue ?? 0.0
let pushType = try resolvePushType(channelId: options?.channelId)
@@ -86,7 +86,7 @@ public class VoltraModuleImpl {
}
func updateLiveActivity(activityId: String, jsonString: String, options: UpdateVoltraOptions?) async throws {
- guard #available(iOS 16.2, *) else { throw VoltraModule.VoltraErrors.unsupportedOS }
+ guard #available(iOS 16.2, *) else { throw VoltraErrors.unsupportedOS }
// Compress JSON using brotli level 2
let compressedJson = try BrotliCompression.compress(jsonString: jsonString)
@@ -94,12 +94,12 @@ public class VoltraModuleImpl {
// Extract staleDate and relevanceScore from options
let staleDate: Date? = {
- if let staleDateMs = options?.staleDate {
+ if let staleDateMs = options?.staleDate?.doubleValue {
return Date(timeIntervalSince1970: staleDateMs / 1000.0)
}
return nil
}()
- let relevanceScore: Double = options?.relevanceScore ?? 0.0
+ let relevanceScore: Double = options?.relevanceScore?.doubleValue ?? 0.0
// Create update request struct with compressed JSON
let updateRequest = UpdateActivityRequest(
@@ -116,7 +116,7 @@ public class VoltraModuleImpl {
}
func endLiveActivity(activityId: String, options: EndVoltraOptions?) async throws {
- guard #available(iOS 16.2, *) else { throw VoltraModule.VoltraErrors.unsupportedOS }
+ guard #available(iOS 16.2, *) else { throw VoltraErrors.unsupportedOS }
// Convert dismissal policy options to ActivityKit type
let dismissalPolicy = convertToActivityKitDismissalPolicy(options?.dismissalPolicy)
@@ -129,7 +129,7 @@ public class VoltraModuleImpl {
}
func endAllLiveActivities() async throws {
- guard #available(iOS 16.2, *) else { throw VoltraModule.VoltraErrors.unsupportedOS }
+ guard #available(iOS 16.2, *) else { throw VoltraErrors.unsupportedOS }
await liveActivityService.endAllActivities()
}
@@ -149,7 +149,7 @@ public class VoltraModuleImpl {
}
func reloadLiveActivities(activityNames: [String]?) async throws {
- guard #available(iOS 16.2, *) else { throw VoltraModule.VoltraErrors.unsupportedOS }
+ guard #available(iOS 16.2, *) else { throw VoltraErrors.unsupportedOS }
let activities = liveActivityService.getAllActivities()
@@ -242,21 +242,21 @@ public class VoltraModuleImpl {
// MARK: - Private Helpers
private func mapError(_ error: Error) -> Error {
- if let moduleError = error as? VoltraModule.VoltraErrors {
+ if let moduleError = error as? VoltraErrors {
return moduleError
}
if let serviceError = error as? VoltraLiveActivityError {
switch serviceError {
case .unsupportedOS:
- return VoltraModule.VoltraErrors.unsupportedOS
+ return VoltraErrors.unsupportedOS
case .liveActivitiesNotEnabled:
- return VoltraModule.VoltraErrors.liveActivitiesNotEnabled
+ return VoltraErrors.liveActivitiesNotEnabled
case .notFound:
- return VoltraModule.VoltraErrors.notFound
+ return VoltraErrors.notFound
}
}
- return VoltraModule.VoltraErrors.unexpectedError(error)
+ return VoltraErrors.unexpectedError(error)
}
private func resolvePushType(channelId rawChannelId: String?) throws -> PushType? {
@@ -266,7 +266,7 @@ public class VoltraModuleImpl {
let channelId = rawChannelId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !channelId.isEmpty else {
- throw VoltraModule.VoltraErrors.unexpectedError(
+ throw VoltraErrors.unexpectedError(
NSError(
domain: "VoltraModule",
code: -20,
@@ -276,7 +276,7 @@ public class VoltraModuleImpl {
}
guard pushNotificationsEnabled else {
- throw VoltraModule.VoltraErrors.unexpectedError(
+ throw VoltraErrors.unexpectedError(
NSError(
domain: "VoltraModule",
code: -21,
@@ -300,7 +300,7 @@ public class VoltraModuleImpl {
VoltraLogger.module.debug("Compressed payload: \(dataSize)B (budget \(safeBudget)B, hard cap \(hardCap)B)")
if dataSize > safeBudget {
- throw VoltraModule.VoltraErrors.unexpectedError(
+ throw VoltraErrors.unexpectedError(
NSError(
domain: "VoltraModule",
code: operation == "start" ? -10 : -11,
@@ -319,7 +319,7 @@ public class VoltraModuleImpl {
case "immediate":
return .immediate
case "after":
- if let timestamp = options.date {
+ if let timestamp = options.date?.doubleValue {
let date = Date(timeIntervalSince1970: timestamp / 1000.0)
return .after(date)
}
diff --git a/packages/ios-client/ios/app/VoltraOptions.swift b/packages/ios-client/ios/app/VoltraOptions.swift
new file mode 100644
index 00000000..c5fed09c
--- /dev/null
+++ b/packages/ios-client/ios/app/VoltraOptions.swift
@@ -0,0 +1,27 @@
+import Foundation
+
+@objc public final class StartVoltraOptions: NSObject {
+ @objc public var activityName: String?
+ @objc public var deepLinkUrl: String?
+ @objc public var staleDate: NSNumber?
+ @objc public var relevanceScore: NSNumber?
+ @objc public var channelId: String?
+}
+
+@objc public final class UpdateVoltraOptions: NSObject {
+ @objc public var staleDate: NSNumber?
+ @objc public var relevanceScore: NSNumber?
+}
+
+@objc public final class DismissalPolicyOptions: NSObject {
+ @objc public var type: String = "immediate"
+ @objc public var date: NSNumber?
+}
+
+@objc public final class EndVoltraOptions: NSObject {
+ @objc public var dismissalPolicy: DismissalPolicyOptions?
+}
+
+@objc public final class UpdateWidgetOptions: NSObject {
+ @objc public var deepLinkUrl: String?
+}
diff --git a/packages/ios-client/ios/app/VoltraViewComponentView.h b/packages/ios-client/ios/app/VoltraViewComponentView.h
new file mode 100644
index 00000000..37af09ba
--- /dev/null
+++ b/packages/ios-client/ios/app/VoltraViewComponentView.h
@@ -0,0 +1,10 @@
+#import
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface VoltraViewComponentView : RCTViewComponentView
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/ios-client/ios/app/VoltraViewComponentView.mm b/packages/ios-client/ios/app/VoltraViewComponentView.mm
new file mode 100644
index 00000000..5cff77e2
--- /dev/null
+++ b/packages/ios-client/ios/app/VoltraViewComponentView.mm
@@ -0,0 +1,67 @@
+#import "VoltraViewComponentView.h"
+
+#if __has_include("Voltra/Voltra-Swift.h")
+#import "Voltra/Voltra-Swift.h"
+#else
+#import "Voltra-Swift.h"
+#endif
+
+#import
+#import
+#import
+
+using namespace facebook::react;
+
+@interface VoltraViewComponentView ()
+@end
+
+@implementation VoltraViewComponentView {
+ VoltraViewRoot *_contentView;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ _contentView = [[VoltraViewRoot alloc] initWithFrame:CGRectZero];
+ [self addSubview:_contentView];
+ }
+ return self;
+}
+
+- (instancetype)init {
+ return [self initWithFrame:CGRectZero];
+}
+
+- (void)updateProps:(Props::Shared const &)props
+ oldProps:(Props::Shared const &)oldProps {
+ const auto &oldViewProps =
+ *std::static_pointer_cast(_props);
+ const auto &newViewProps =
+ *std::static_pointer_cast(props);
+
+ if (oldViewProps.viewId != newViewProps.viewId) {
+ NSString *viewId =
+ [[NSString alloc] initWithUTF8String:newViewProps.viewId.c_str()]
+ ?: @"";
+ [_contentView setViewId:viewId];
+ }
+
+ if (oldViewProps.payload != newViewProps.payload) {
+ NSString *payload =
+ [[NSString alloc] initWithUTF8String:newViewProps.payload.c_str()]
+ ?: @"";
+ [_contentView setPayload:payload];
+ }
+
+ [super updateProps:props oldProps:oldProps];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ _contentView.frame = self.bounds;
+}
+
++ (ComponentDescriptorProvider)componentDescriptorProvider {
+ return concreteComponentDescriptorProvider();
+}
+
+@end
diff --git a/packages/voltra/ios/app/VoltraRN.swift b/packages/ios-client/ios/app/VoltraViewRoot.swift
similarity index 71%
rename from packages/voltra/ios/app/VoltraRN.swift
rename to packages/ios-client/ios/app/VoltraViewRoot.swift
index 6f4a2c3e..5ca7a267 100644
--- a/packages/voltra/ios/app/VoltraRN.swift
+++ b/packages/ios-client/ios/app/VoltraViewRoot.swift
@@ -1,17 +1,20 @@
-import ExpoModulesCore
import os
import SwiftUI
import UIKit
-class VoltraRN: ExpoView {
+@objc public class VoltraViewRoot: UIView {
private var hostingController: UIHostingController?
private var root: VoltraNode = .empty
-
- /// Unique identifier for this view instance, used as 'source' in interaction events
private var viewId: String = UUID().uuidString
- required init(appContext: AppContext? = nil) {
- super.init(appContext: appContext)
+ override public init(frame: CGRect) {
+ super.init(frame: frame)
+ clipsToBounds = true
+ setupHostingController()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
clipsToBounds = true
setupHostingController()
}
@@ -24,22 +27,18 @@ class VoltraRN: ExpoView {
self.hostingController = hostingController
}
- override func layoutSubviews() {
+ override public func layoutSubviews() {
super.layoutSubviews()
hostingController?.view.frame = bounds
}
- func setViewId(_ id: String) {
+ @objc public func setViewId(_ id: String) {
guard !id.isEmpty else { return }
viewId = id
updateView()
}
- func setPayload(_ jsonString: String) {
- parseAndUpdatePayload(jsonString)
- }
-
- private func parseAndUpdatePayload(_ jsonString: String) {
+ @objc public func setPayload(_ jsonString: String) {
do {
let json = try JSONValue.parse(from: jsonString)
root = VoltraNode.parse(from: json)
@@ -47,14 +46,12 @@ class VoltraRN: ExpoView {
VoltraLogger.module.error("Failed to parse payload in VoltraView: \(error)")
root = .empty
}
-
updateView()
}
private func updateView() {
hostingController?.view.removeFromSuperview()
- // This is not the most performant way to update the view, but it's the easiest way to get the job done.
let newView = Voltra(root: root, activityId: viewId)
let newHostingController = UIHostingController(rootView: AnyView(newView))
newHostingController.view.backgroundColor = .clear
diff --git a/packages/voltra/ios/app/VoltraWidgetService.swift b/packages/ios-client/ios/app/VoltraWidgetService.swift
similarity index 100%
rename from packages/voltra/ios/app/VoltraWidgetService.swift
rename to packages/ios-client/ios/app/VoltraWidgetService.swift
diff --git a/packages/voltra/ios/shared/BrotliCompression.swift b/packages/ios-client/ios/shared/BrotliCompression.swift
similarity index 100%
rename from packages/voltra/ios/shared/BrotliCompression.swift
rename to packages/ios-client/ios/shared/BrotliCompression.swift
diff --git a/packages/voltra/ios/shared/ComponentTypeID.swift b/packages/ios-client/ios/shared/ComponentTypeID.swift
similarity index 100%
rename from packages/voltra/ios/shared/ComponentTypeID.swift
rename to packages/ios-client/ios/shared/ComponentTypeID.swift
diff --git a/packages/voltra/ios/shared/Data+hexString.swift b/packages/ios-client/ios/shared/Data+hexString.swift
similarity index 100%
rename from packages/voltra/ios/shared/Data+hexString.swift
rename to packages/ios-client/ios/shared/Data+hexString.swift
diff --git a/packages/voltra/ios/shared/Date+toTimerInterval.swift b/packages/ios-client/ios/shared/Date+toTimerInterval.swift
similarity index 100%
rename from packages/voltra/ios/shared/Date+toTimerInterval.swift
rename to packages/ios-client/ios/shared/Date+toTimerInterval.swift
diff --git a/packages/voltra/ios/shared/JSONValue.swift b/packages/ios-client/ios/shared/JSONValue.swift
similarity index 100%
rename from packages/voltra/ios/shared/JSONValue.swift
rename to packages/ios-client/ios/shared/JSONValue.swift
diff --git a/packages/voltra/ios/shared/ShortNames.swift b/packages/ios-client/ios/shared/ShortNames.swift
similarity index 100%
rename from packages/voltra/ios/shared/ShortNames.swift
rename to packages/ios-client/ios/shared/ShortNames.swift
diff --git a/packages/voltra/ios/shared/VoltraAttributes.swift b/packages/ios-client/ios/shared/VoltraAttributes.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraAttributes.swift
rename to packages/ios-client/ios/shared/VoltraAttributes.swift
diff --git a/packages/voltra/ios/shared/VoltraConfig.swift b/packages/ios-client/ios/shared/VoltraConfig.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraConfig.swift
rename to packages/ios-client/ios/shared/VoltraConfig.swift
diff --git a/packages/voltra/ios/shared/VoltraConstants.swift b/packages/ios-client/ios/shared/VoltraConstants.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraConstants.swift
rename to packages/ios-client/ios/shared/VoltraConstants.swift
diff --git a/packages/voltra/ios/shared/VoltraElement.swift b/packages/ios-client/ios/shared/VoltraElement.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraElement.swift
rename to packages/ios-client/ios/shared/VoltraElement.swift
diff --git a/packages/voltra/ios/shared/VoltraEvent.swift b/packages/ios-client/ios/shared/VoltraEvent.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraEvent.swift
rename to packages/ios-client/ios/shared/VoltraEvent.swift
diff --git a/packages/voltra/ios/shared/VoltraEventBus.swift b/packages/ios-client/ios/shared/VoltraEventBus.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraEventBus.swift
rename to packages/ios-client/ios/shared/VoltraEventBus.swift
diff --git a/packages/voltra/ios/shared/VoltraImageStore.swift b/packages/ios-client/ios/shared/VoltraImageStore.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraImageStore.swift
rename to packages/ios-client/ios/shared/VoltraImageStore.swift
diff --git a/packages/voltra/ios/shared/VoltraInitialStateLocale.swift b/packages/ios-client/ios/shared/VoltraInitialStateLocale.swift
similarity index 92%
rename from packages/voltra/ios/shared/VoltraInitialStateLocale.swift
rename to packages/ios-client/ios/shared/VoltraInitialStateLocale.swift
index cdc17572..0b00e867 100644
--- a/packages/voltra/ios/shared/VoltraInitialStateLocale.swift
+++ b/packages/ios-client/ios/shared/VoltraInitialStateLocale.swift
@@ -1,6 +1,6 @@
import Foundation
-/// Matches `packages/expo-plugin/src/utils/localePick.ts` fallback order for bundled widget initial JSON.
+/// Matches `@use-voltra/expo-plugin` `localePick` fallback order for bundled widget initial JSON.
public enum VoltraInitialStateLocale {
public static func pickJson(from perLocale: [String: String], preferredLanguages: [String]) -> String? {
let entries = perLocale.filter { !$0.value.isEmpty }
diff --git a/packages/voltra/ios/shared/VoltraInteractionIntent.swift b/packages/ios-client/ios/shared/VoltraInteractionIntent.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraInteractionIntent.swift
rename to packages/ios-client/ios/shared/VoltraInteractionIntent.swift
diff --git a/packages/voltra/ios/shared/VoltraKeychainHelper.swift b/packages/ios-client/ios/shared/VoltraKeychainHelper.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraKeychainHelper.swift
rename to packages/ios-client/ios/shared/VoltraKeychainHelper.swift
diff --git a/packages/voltra/ios/shared/VoltraLiveActivityPayload.swift b/packages/ios-client/ios/shared/VoltraLiveActivityPayload.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraLiveActivityPayload.swift
rename to packages/ios-client/ios/shared/VoltraLiveActivityPayload.swift
diff --git a/packages/voltra/ios/shared/VoltraLogger.swift b/packages/ios-client/ios/shared/VoltraLogger.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraLogger.swift
rename to packages/ios-client/ios/shared/VoltraLogger.swift
diff --git a/packages/voltra/ios/shared/VoltraNode.swift b/packages/ios-client/ios/shared/VoltraNode.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraNode.swift
rename to packages/ios-client/ios/shared/VoltraNode.swift
diff --git a/packages/voltra/ios/shared/VoltraPayloadMigrator.swift b/packages/ios-client/ios/shared/VoltraPayloadMigrator.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraPayloadMigrator.swift
rename to packages/ios-client/ios/shared/VoltraPayloadMigrator.swift
diff --git a/packages/voltra/ios/shared/VoltraPersistentEventQueue.swift b/packages/ios-client/ios/shared/VoltraPersistentEventQueue.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraPersistentEventQueue.swift
rename to packages/ios-client/ios/shared/VoltraPersistentEventQueue.swift
diff --git a/packages/voltra/ios/shared/VoltraRefreshIntent.swift b/packages/ios-client/ios/shared/VoltraRefreshIntent.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraRefreshIntent.swift
rename to packages/ios-client/ios/shared/VoltraRefreshIntent.swift
diff --git a/packages/voltra/ios/shared/VoltraRegion.swift b/packages/ios-client/ios/shared/VoltraRegion.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraRegion.swift
rename to packages/ios-client/ios/shared/VoltraRegion.swift
diff --git a/packages/voltra/ios/shared/VoltraWidgetDefaults.swift b/packages/ios-client/ios/shared/VoltraWidgetDefaults.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraWidgetDefaults.swift
rename to packages/ios-client/ios/shared/VoltraWidgetDefaults.swift
diff --git a/packages/voltra/ios/shared/VoltraWidgetServerFetcher.swift b/packages/ios-client/ios/shared/VoltraWidgetServerFetcher.swift
similarity index 100%
rename from packages/voltra/ios/shared/VoltraWidgetServerFetcher.swift
rename to packages/ios-client/ios/shared/VoltraWidgetServerFetcher.swift
diff --git a/packages/voltra/ios/target/VoltraHomeWidget.swift b/packages/ios-client/ios/target/VoltraHomeWidget.swift
similarity index 100%
rename from packages/voltra/ios/target/VoltraHomeWidget.swift
rename to packages/ios-client/ios/target/VoltraHomeWidget.swift
diff --git a/packages/voltra/ios/target/VoltraWidget.swift b/packages/ios-client/ios/target/VoltraWidget.swift
similarity index 100%
rename from packages/voltra/ios/target/VoltraWidget.swift
rename to packages/ios-client/ios/target/VoltraWidget.swift
diff --git a/packages/voltra/ios/ui/Extensions/View.modifiers.swift b/packages/ios-client/ios/ui/Extensions/View.modifiers.swift
similarity index 100%
rename from packages/voltra/ios/ui/Extensions/View.modifiers.swift
rename to packages/ios-client/ios/ui/Extensions/View.modifiers.swift
diff --git a/packages/voltra/ios/ui/Extensions/VoltraNode+Parameters.swift b/packages/ios-client/ios/ui/Extensions/VoltraNode+Parameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Extensions/VoltraNode+Parameters.swift
rename to packages/ios-client/ios/ui/Extensions/VoltraNode+Parameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/.generated b/packages/ios-client/ios/ui/Generated/Parameters/.generated
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/.generated
rename to packages/ios-client/ios/ui/Generated/Parameters/.generated
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ButtonParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ButtonParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ButtonParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ButtonParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ChartParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ChartParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ChartParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ChartParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/CircularProgressViewParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/CircularProgressViewParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/CircularProgressViewParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/CircularProgressViewParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ComponentParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ComponentParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ComponentParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ComponentParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/DividerParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/DividerParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/DividerParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/DividerParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/GaugeParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/GaugeParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/GaugeParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/GaugeParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/GlassContainerParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/GlassContainerParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/GlassContainerParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/GlassContainerParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/GroupBoxParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/GroupBoxParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/GroupBoxParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/GroupBoxParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/HStackParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/HStackParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/HStackParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/HStackParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ImageParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ImageParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ImageParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ImageParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/LabelParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/LabelParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/LabelParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/LabelParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/LinearGradientParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/LinearGradientParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/LinearGradientParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/LinearGradientParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/LinkParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/LinkParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/LinkParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/LinkParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/MaskParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/MaskParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/MaskParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/MaskParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/SpacerParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/SpacerParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/SpacerParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/SpacerParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/SymbolParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/SymbolParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/SymbolParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/SymbolParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/TextParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/TextParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/TextParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/TextParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/TimerParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/TimerParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/TimerParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/TimerParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ToggleParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ToggleParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ToggleParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ToggleParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/VStackParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/VStackParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/VStackParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/VStackParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ViewParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ViewParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ViewParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ViewParameters.swift
diff --git a/packages/voltra/ios/ui/Generated/Parameters/ZStackParameters.swift b/packages/ios-client/ios/ui/Generated/Parameters/ZStackParameters.swift
similarity index 100%
rename from packages/voltra/ios/ui/Generated/Parameters/ZStackParameters.swift
rename to packages/ios-client/ios/ui/Generated/Parameters/ZStackParameters.swift
diff --git a/packages/voltra/ios/ui/Helpers/VoltraDeepLinkResolver.swift b/packages/ios-client/ios/ui/Helpers/VoltraDeepLinkResolver.swift
similarity index 100%
rename from packages/voltra/ios/ui/Helpers/VoltraDeepLinkResolver.swift
rename to packages/ios-client/ios/ui/Helpers/VoltraDeepLinkResolver.swift
diff --git a/packages/voltra/ios/ui/Helpers/VoltraProgressDriver.swift b/packages/ios-client/ios/ui/Helpers/VoltraProgressDriver.swift
similarity index 100%
rename from packages/voltra/ios/ui/Helpers/VoltraProgressDriver.swift
rename to packages/ios-client/ios/ui/Helpers/VoltraProgressDriver.swift
diff --git a/packages/voltra/ios/ui/Layout/FlexContainerHelper.swift b/packages/ios-client/ios/ui/Layout/FlexContainerHelper.swift
similarity index 100%
rename from packages/voltra/ios/ui/Layout/FlexContainerHelper.swift
rename to packages/ios-client/ios/ui/Layout/FlexContainerHelper.swift
diff --git a/packages/voltra/ios/ui/Layout/VoltraFlexStackLayout.swift b/packages/ios-client/ios/ui/Layout/VoltraFlexStackLayout.swift
similarity index 100%
rename from packages/voltra/ios/ui/Layout/VoltraFlexStackLayout.swift
rename to packages/ios-client/ios/ui/Layout/VoltraFlexStackLayout.swift
diff --git a/packages/voltra/ios/ui/Protocols/VoltraView.swift b/packages/ios-client/ios/ui/Protocols/VoltraView.swift
similarity index 100%
rename from packages/voltra/ios/ui/Protocols/VoltraView.swift
rename to packages/ios-client/ios/ui/Protocols/VoltraView.swift
diff --git a/packages/voltra/ios/ui/Style/BackgroundValue.swift b/packages/ios-client/ios/ui/Style/BackgroundValue.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/BackgroundValue.swift
rename to packages/ios-client/ios/ui/Style/BackgroundValue.swift
diff --git a/packages/voltra/ios/ui/Style/CompositeStyle.swift b/packages/ios-client/ios/ui/Style/CompositeStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/CompositeStyle.swift
rename to packages/ios-client/ios/ui/Style/CompositeStyle.swift
diff --git a/packages/voltra/ios/ui/Style/DecorationStyle.swift b/packages/ios-client/ios/ui/Style/DecorationStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/DecorationStyle.swift
rename to packages/ios-client/ios/ui/Style/DecorationStyle.swift
diff --git a/packages/voltra/ios/ui/Style/FlexEnvironment.swift b/packages/ios-client/ios/ui/Style/FlexEnvironment.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/FlexEnvironment.swift
rename to packages/ios-client/ios/ui/Style/FlexEnvironment.swift
diff --git a/packages/voltra/ios/ui/Style/FontVariant.swift b/packages/ios-client/ios/ui/Style/FontVariant.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/FontVariant.swift
rename to packages/ios-client/ios/ui/Style/FontVariant.swift
diff --git a/packages/voltra/ios/ui/Style/GlassEffect.swift b/packages/ios-client/ios/ui/Style/GlassEffect.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/GlassEffect.swift
rename to packages/ios-client/ios/ui/Style/GlassEffect.swift
diff --git a/packages/voltra/ios/ui/Style/JSColorParser.swift b/packages/ios-client/ios/ui/Style/JSColorParser.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/JSColorParser.swift
rename to packages/ios-client/ios/ui/Style/JSColorParser.swift
diff --git a/packages/voltra/ios/ui/Style/JSGradientParser.swift b/packages/ios-client/ios/ui/Style/JSGradientParser.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/JSGradientParser.swift
rename to packages/ios-client/ios/ui/Style/JSGradientParser.swift
diff --git a/packages/voltra/ios/ui/Style/JSStyleParser.swift b/packages/ios-client/ios/ui/Style/JSStyleParser.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/JSStyleParser.swift
rename to packages/ios-client/ios/ui/Style/JSStyleParser.swift
diff --git a/packages/voltra/ios/ui/Style/LayoutStyle.swift b/packages/ios-client/ios/ui/Style/LayoutStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/LayoutStyle.swift
rename to packages/ios-client/ios/ui/Style/LayoutStyle.swift
diff --git a/packages/voltra/ios/ui/Style/Overflow.swift b/packages/ios-client/ios/ui/Style/Overflow.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/Overflow.swift
rename to packages/ios-client/ios/ui/Style/Overflow.swift
diff --git a/packages/voltra/ios/ui/Style/RenderingStyle.swift b/packages/ios-client/ios/ui/Style/RenderingStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/RenderingStyle.swift
rename to packages/ios-client/ios/ui/Style/RenderingStyle.swift
diff --git a/packages/voltra/ios/ui/Style/StyleConverter.swift b/packages/ios-client/ios/ui/Style/StyleConverter.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/StyleConverter.swift
rename to packages/ios-client/ios/ui/Style/StyleConverter.swift
diff --git a/packages/voltra/ios/ui/Style/TextDecoration.swift b/packages/ios-client/ios/ui/Style/TextDecoration.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/TextDecoration.swift
rename to packages/ios-client/ios/ui/Style/TextDecoration.swift
diff --git a/packages/voltra/ios/ui/Style/TextStyle.swift b/packages/ios-client/ios/ui/Style/TextStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/TextStyle.swift
rename to packages/ios-client/ios/ui/Style/TextStyle.swift
diff --git a/packages/voltra/ios/ui/Style/View+applyStyle.swift b/packages/ios-client/ios/ui/Style/View+applyStyle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Style/View+applyStyle.swift
rename to packages/ios-client/ios/ui/Style/View+applyStyle.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraButton.swift b/packages/ios-client/ios/ui/Views/VoltraButton.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraButton.swift
rename to packages/ios-client/ios/ui/Views/VoltraButton.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraChart.swift b/packages/ios-client/ios/ui/Views/VoltraChart.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraChart.swift
rename to packages/ios-client/ios/ui/Views/VoltraChart.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraCircularProgressView.swift b/packages/ios-client/ios/ui/Views/VoltraCircularProgressView.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraCircularProgressView.swift
rename to packages/ios-client/ios/ui/Views/VoltraCircularProgressView.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraDivider.swift b/packages/ios-client/ios/ui/Views/VoltraDivider.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraDivider.swift
rename to packages/ios-client/ios/ui/Views/VoltraDivider.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraFlexView.swift b/packages/ios-client/ios/ui/Views/VoltraFlexView.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraFlexView.swift
rename to packages/ios-client/ios/ui/Views/VoltraFlexView.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraGauge.swift b/packages/ios-client/ios/ui/Views/VoltraGauge.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraGauge.swift
rename to packages/ios-client/ios/ui/Views/VoltraGauge.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraGlassContainer.swift b/packages/ios-client/ios/ui/Views/VoltraGlassContainer.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraGlassContainer.swift
rename to packages/ios-client/ios/ui/Views/VoltraGlassContainer.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraGroupBox.swift b/packages/ios-client/ios/ui/Views/VoltraGroupBox.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraGroupBox.swift
rename to packages/ios-client/ios/ui/Views/VoltraGroupBox.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraHStack.swift b/packages/ios-client/ios/ui/Views/VoltraHStack.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraHStack.swift
rename to packages/ios-client/ios/ui/Views/VoltraHStack.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraImage.swift b/packages/ios-client/ios/ui/Views/VoltraImage.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraImage.swift
rename to packages/ios-client/ios/ui/Views/VoltraImage.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraLabel.swift b/packages/ios-client/ios/ui/Views/VoltraLabel.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraLabel.swift
rename to packages/ios-client/ios/ui/Views/VoltraLabel.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraLinearGradient.swift b/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraLinearGradient.swift
rename to packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraLinearProgressView.swift b/packages/ios-client/ios/ui/Views/VoltraLinearProgressView.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraLinearProgressView.swift
rename to packages/ios-client/ios/ui/Views/VoltraLinearProgressView.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraLink.swift b/packages/ios-client/ios/ui/Views/VoltraLink.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraLink.swift
rename to packages/ios-client/ios/ui/Views/VoltraLink.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraMask.swift b/packages/ios-client/ios/ui/Views/VoltraMask.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraMask.swift
rename to packages/ios-client/ios/ui/Views/VoltraMask.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraSpacer.swift b/packages/ios-client/ios/ui/Views/VoltraSpacer.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraSpacer.swift
rename to packages/ios-client/ios/ui/Views/VoltraSpacer.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraSymbol.swift b/packages/ios-client/ios/ui/Views/VoltraSymbol.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraSymbol.swift
rename to packages/ios-client/ios/ui/Views/VoltraSymbol.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraText.swift b/packages/ios-client/ios/ui/Views/VoltraText.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraText.swift
rename to packages/ios-client/ios/ui/Views/VoltraText.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraTimer.swift b/packages/ios-client/ios/ui/Views/VoltraTimer.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraTimer.swift
rename to packages/ios-client/ios/ui/Views/VoltraTimer.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraToggle.swift b/packages/ios-client/ios/ui/Views/VoltraToggle.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraToggle.swift
rename to packages/ios-client/ios/ui/Views/VoltraToggle.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraVStack.swift b/packages/ios-client/ios/ui/Views/VoltraVStack.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraVStack.swift
rename to packages/ios-client/ios/ui/Views/VoltraVStack.swift
diff --git a/packages/voltra/ios/ui/Views/VoltraZStack.swift b/packages/ios-client/ios/ui/Views/VoltraZStack.swift
similarity index 100%
rename from packages/voltra/ios/ui/Views/VoltraZStack.swift
rename to packages/ios-client/ios/ui/Views/VoltraZStack.swift
diff --git a/packages/voltra/ios/ui/Voltra.swift b/packages/ios-client/ios/ui/Voltra.swift
similarity index 100%
rename from packages/voltra/ios/ui/Voltra.swift
rename to packages/ios-client/ios/ui/Voltra.swift
diff --git a/packages/ios-client/package.json b/packages/ios-client/package.json
index a5627f68..028b7d21 100644
--- a/packages/ios-client/package.json
+++ b/packages/ios-client/package.json
@@ -2,30 +2,65 @@
"name": "@use-voltra/ios-client",
"version": "1.4.1",
"description": "Client-only Voltra APIs for iOS",
- "main": "build/cjs/index.js",
- "module": "build/esm/index.js",
- "types": "build/types/index.d.ts",
+ "main": "./build/commonjs/index.js",
+ "module": "./build/module/index.js",
+ "types": "./build/typescript/module/index.d.ts",
"exports": {
".": {
- "types": "./build/types/index.d.ts",
- "require": "./build/cjs/index.js",
- "import": "./build/esm/index.js",
- "default": "./build/esm/index.js"
+ "source": "./src/index.ts",
+ "import": {
+ "types": "./build/typescript/module/index.d.ts",
+ "default": "./build/module/index.js"
+ },
+ "require": {
+ "types": "./build/typescript/commonjs/index.d.ts",
+ "default": "./build/commonjs/index.js"
+ },
+ "default": "./build/module/index.js"
},
- "./package.json": "./package.json"
+ "./package.json": "./package.json",
+ "./app.plugin.js": "./expo-plugin/app.plugin.js"
},
"files": [
"build",
+ "expo-plugin",
+ "ios",
+ "Voltra.podspec",
"README.md"
],
"scripts": {
- "build": "node ../../scripts/build-package.mjs packages/ios-client",
- "clean": "rm -rf build",
- "lint": "oxlint src",
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
+ "build": "npm run clean && npm run build:expo-plugin && bob build",
+ "build:expo-plugin": "node ../../scripts/build-package.mjs packages/ios-client/expo-plugin",
+ "clean": "rm -rf build && npm run clean:expo-plugin",
+ "clean:expo-plugin": "rm -rf expo-plugin/build",
+ "format:check": "npm run format:swift:check",
+ "format:fix": "npm run format:swift:fix",
+ "format:swift:check": "swiftformat --lint ios",
+ "format:swift:fix": "swiftformat ios",
+ "lint": "oxlint src expo-plugin/src",
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit && tsc -p expo-plugin/tsconfig.typecheck.json --noEmit",
+ "test": "jest --config expo-plugin/jest.config.js",
+ "test:swift": "cd ios && swift test"
},
"dependencies": {
- "@use-voltra/ios": "1.4.1"
+ "@babel/core": "^7.27.4",
+ "@expo/config-plugins": "~10.1.2",
+ "@expo/plist": "^0.3.5",
+ "@use-voltra/expo-plugin": "1.4.1",
+ "@use-voltra/ios": "1.4.1",
+ "dedent": "^1.7.1",
+ "xcode": "^3.0.1"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.14",
+ "@types/node": "^20.19.25",
+ "jest": "^29.7.0",
+ "ts-jest": "^29.3.4"
},
"keywords": [
"react-native",
@@ -45,9 +80,46 @@
},
"license": "MIT",
"homepage": "https://use-voltra.dev",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "build",
+ "exclude": "**/{__tests__,__fixtures__,__mocks__,__rsc_tests__}/**",
+ "targets": [
+ [
+ "module",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "commonjs",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ },
+ "codegenConfig": {
+ "name": "VoltraSpec",
+ "type": "all",
+ "jsSrcsDir": "src",
+ "ios": {
+ "modulesProvider": {
+ "NativeVoltra": "NativeVoltra"
+ },
+ "components": {
+ "VoltraView": {
+ "className": "VoltraViewComponentView"
+ }
+ }
+ }
}
}
diff --git a/packages/ios-client/src/VoltraModule.ts b/packages/ios-client/src/VoltraModule.ts
index 6a9bab06..9e67fbc7 100644
--- a/packages/ios-client/src/VoltraModule.ts
+++ b/packages/ios-client/src/VoltraModule.ts
@@ -1,57 +1,2 @@
-import { requireNativeModule } from 'expo'
-
-import type {
- EventSubscription,
- PreloadImageOptions,
- PreloadImagesResult,
- UpdateWidgetOptions,
- WidgetServerCredentials,
-} from './types.js'
-
-export type StartVoltraOptions = {
- target?: string
- deepLinkUrl?: string
- activityName?: string
- staleDate?: number
- relevanceScore?: number
- channelId?: string
-}
-
-export type UpdateVoltraOptions = {
- staleDate?: number
- relevanceScore?: number
-}
-
-export type EndVoltraOptions = {
- dismissalPolicy?: {
- type: 'immediate' | 'after'
- date?: number
- }
-}
-
-export interface VoltraIOSModuleSpec {
- startLiveActivity(jsonString: string, options?: StartVoltraOptions): Promise
- updateLiveActivity(activityId: string, jsonString: string, options?: UpdateVoltraOptions): Promise
- endLiveActivity(activityId: string, options?: EndVoltraOptions): Promise
- endAllLiveActivities(): Promise
- getLatestVoltraActivityId(): Promise
- listVoltraActivityIds(): Promise
- isLiveActivityActive(activityName: string): boolean
- isHeadless(): boolean
- preloadImages(images: PreloadImageOptions[]): Promise
- reloadLiveActivities(activityNames?: string[] | null): Promise
- clearPreloadedImages(keys?: string[] | null): Promise
- updateWidget(widgetId: string, jsonString: string, options?: UpdateWidgetOptions): Promise
- scheduleWidget(widgetId: string, timelineJson: string): Promise
- reloadWidgets(widgetIds?: string[] | null): Promise
- clearWidget(widgetId: string): Promise
- clearAllWidgets(): Promise
- getActiveWidgets(): Promise
- setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise
- clearWidgetServerCredentials(): Promise
- addListener(event: string, listener: (event: any) => void): EventSubscription
-}
-
-const VoltraModule = requireNativeModule('VoltraModule')
-
-export default VoltraModule
+export type { Spec as VoltraIOSModuleSpec } from './native/NativeVoltra'
+export { getNativeVoltra } from './native/NativeVoltra'
diff --git a/packages/ios-client/src/components/VoltraView.tsx b/packages/ios-client/src/components/VoltraView.tsx
index 228525bc..bfb6e7ed 100644
--- a/packages/ios-client/src/components/VoltraView.tsx
+++ b/packages/ios-client/src/components/VoltraView.tsx
@@ -1,12 +1,11 @@
-import { requireNativeView } from 'expo'
import React, { type ReactNode, useEffect, useMemo } from 'react'
-import { type StyleProp, View, type ViewStyle } from 'react-native'
+import { type StyleProp, type ViewStyle } from 'react-native'
import { renderVoltraVariantToJson } from '@use-voltra/ios'
-import { addVoltraListener, type VoltraInteractionEvent } from '../events.js'
+import VoltraFabricView from '../native/VoltraRNNativeComponent'
-const NativeVoltraView = requireNativeView('VoltraModule')
+import { addVoltraListener, type VoltraInteractionEvent } from '../events.js'
const generateViewId = () => `voltra-view-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
@@ -46,9 +45,5 @@ export function VoltraView({ id, children, style, onInteraction, testID }: Voltr
return () => subscription.remove()
}, [viewId, onInteraction])
- return (
-
-
-
- )
+ return
}
diff --git a/packages/ios-client/src/events.ts b/packages/ios-client/src/events.ts
index db0f7c93..171251dc 100644
--- a/packages/ios-client/src/events.ts
+++ b/packages/ios-client/src/events.ts
@@ -1,7 +1,7 @@
import { Platform } from 'react-native'
import type { EventSubscription } from './types.js'
-import VoltraModule from './VoltraModule.js'
+import { getNativeVoltra } from './VoltraModule.js'
export type BasicVoltraEvent = {
source: string
@@ -50,5 +50,21 @@ export function addVoltraListener(
return noopSubscription
}
- return VoltraModule.addListener(event, listener)
+ const voltraModule = getNativeVoltra()
+
+ switch (event) {
+ case 'activityTokenReceived':
+ return voltraModule.onActivityTokenReceived(listener as (arg: VoltraActivityTokenReceivedEvent) => void)
+ case 'activityPushToStartTokenReceived':
+ return voltraModule.onActivityPushToStartTokenReceived(
+ listener as (arg: VoltraActivityPushToStartTokenReceivedEvent) => void
+ )
+ case 'stateChange':
+ return voltraModule.onStateChanged(listener as (arg: VoltraActivityUpdateEvent) => void)
+ case 'interaction':
+ return voltraModule.onInteraction(listener as (arg: VoltraInteractionEvent) => void)
+ default:
+ console.warn(`[Voltra] Event '${event}' is not supported. Returning no-op subscription.`)
+ return noopSubscription
+ }
}
diff --git a/packages/ios-client/src/helpers.ts b/packages/ios-client/src/helpers.ts
index eefa16bf..f2c74bd7 100644
--- a/packages/ios-client/src/helpers.ts
+++ b/packages/ios-client/src/helpers.ts
@@ -1,6 +1,6 @@
import { Platform } from 'react-native'
-import VoltraModule from './VoltraModule.js'
+import { getNativeVoltra } from './VoltraModule.js'
export function isGlassSupported(): boolean {
if (Platform.OS !== 'ios') return false
@@ -17,5 +17,5 @@ export function isGlassSupported(): boolean {
export function isHeadless(): boolean {
if (Platform.OS !== 'ios') return false
- return VoltraModule.isHeadless?.() ?? false
+ return getNativeVoltra().isHeadless?.() ?? false
}
diff --git a/packages/ios-client/src/index.ts b/packages/ios-client/src/index.ts
index 99fd0bd0..e376039d 100644
--- a/packages/ios-client/src/index.ts
+++ b/packages/ios-client/src/index.ts
@@ -1,3 +1,4 @@
+export { Voltra } from '@use-voltra/ios'
export {
VoltraLiveActivityPreview,
type VoltraLiveActivityPreviewProps,
@@ -6,6 +7,7 @@ export { VoltraView, type VoltraViewProps } from './components/VoltraView.js'
export { VoltraWidgetPreview, type VoltraWidgetPreviewProps } from './components/VoltraWidgetPreview.js'
export * from './events.js'
export { isGlassSupported, isHeadless } from './helpers.js'
+export { logger, type VoltraLogLevel } from './logger.js'
export {
endAllLiveActivities,
type EndLiveActivityOptions,
@@ -28,6 +30,9 @@ export {
type PreloadImagesResult,
reloadLiveActivities,
} from './preload.js'
+export { assertRunningOnApple } from './utils/assertRunningOnApple.js'
+export { useUpdateOnHMR } from './utils/useUpdateOnHMR.js'
+export * from './utils/helpers.js'
export type { VoltraElementJson, VoltraNodeJson } from './types.js'
export {
clearWidgetServerCredentials,
diff --git a/packages/ios-client/src/live-activity/api.ts b/packages/ios-client/src/live-activity/api.ts
index 74a597cf..5d43833d 100644
--- a/packages/ios-client/src/live-activity/api.ts
+++ b/packages/ios-client/src/live-activity/api.ts
@@ -5,7 +5,7 @@ import { renderLiveActivityToString, type DismissalPolicy, type LiveActivityVari
import { addVoltraListener } from '../events.js'
import { logger } from '../logger.js'
import { assertRunningOnApple, useUpdateOnHMR } from '../utils/index.js'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltra } from '../VoltraModule.js'
export type SharedLiveActivityOptions = {
staleDate?: number
@@ -182,7 +182,7 @@ export const startLiveActivity = async (
const payload = renderLiveActivityToString(variants)
const normalizedSharedOptions = normalizeSharedLiveActivityOptions(options)
- const targetId = await VoltraModule.startLiveActivity(payload, {
+ const targetId = await getNativeVoltra().startLiveActivity(payload, {
target: 'liveActivity',
deepLinkUrl: options?.deepLinkUrl,
activityName: options?.activityName,
@@ -202,23 +202,23 @@ export const updateLiveActivity = async (
const payload = renderLiveActivityToString(variants)
const normalizedSharedOptions = normalizeSharedLiveActivityOptions(options)
- return VoltraModule.updateLiveActivity(targetId, payload, normalizedSharedOptions)
+ return getNativeVoltra().updateLiveActivity(targetId, payload, normalizedSharedOptions ?? {})
}
export const stopLiveActivity = async (targetId: string, options?: EndLiveActivityOptions): Promise => {
if (!assertRunningOnApple()) return Promise.resolve()
const normalizedOptions = normalizeEndLiveActivityOptions(options)
- return VoltraModule.endLiveActivity(targetId, normalizedOptions)
+ return getNativeVoltra().endLiveActivity(targetId, normalizedOptions ?? { dismissalPolicy: { type: 'immediate' } })
}
export const isLiveActivityActive = (activityName: string): boolean => {
if (!assertRunningOnApple()) return false
- return VoltraModule.isLiveActivityActive(activityName)
+ return getNativeVoltra().isLiveActivityActive(activityName)
}
export async function endAllLiveActivities(): Promise {
if (!assertRunningOnApple()) return Promise.resolve()
- return VoltraModule.endAllLiveActivities()
+ return getNativeVoltra().endAllLiveActivities()
}
diff --git a/packages/ios-client/src/native/NativeVoltra.ts b/packages/ios-client/src/native/NativeVoltra.ts
new file mode 100644
index 00000000..a72309fa
--- /dev/null
+++ b/packages/ios-client/src/native/NativeVoltra.ts
@@ -0,0 +1,118 @@
+import type { CodegenTypes, TurboModule } from 'react-native'
+import { TurboModuleRegistry } from 'react-native'
+
+type VoltraActivityTokenReceivedEvent = Readonly<{
+ source: string
+ timestamp: number
+ type: 'activityTokenReceived'
+ activityName: string
+ pushToken: string
+}>
+
+type VoltraActivityPushToStartTokenReceivedEvent = Readonly<{
+ source: string
+ timestamp: number
+ type: 'activityPushToStartTokenReceived'
+ pushToStartToken: string
+}>
+
+type VoltraActivityUpdateEvent = Readonly<{
+ source: string
+ timestamp: number
+ type: 'stateChange'
+ activityName: string
+ activityState: string
+}>
+
+type VoltraInteractionEvent = Readonly<{
+ source: string
+ timestamp: number
+ type: 'interaction'
+ identifier: string
+ payload: string
+}>
+
+type StartVoltraOptions = Readonly<{
+ target?: string
+ deepLinkUrl?: string
+ activityName?: string
+ staleDate?: number
+ relevanceScore?: number
+ channelId?: string
+}>
+
+type UpdateVoltraOptions = Readonly<{
+ staleDate?: number
+ relevanceScore?: number
+}>
+
+type DismissalPolicyOptions = Readonly<{
+ type: string
+ date?: number
+}>
+
+type EndVoltraOptions = Readonly<{
+ dismissalPolicy?: DismissalPolicyOptions
+}>
+
+type UpdateWidgetOptions = Readonly<{
+ deepLinkUrl?: string
+}>
+
+type PreloadImageOptions = Readonly<{
+ url: string
+ key: string
+ method?: string
+ headers?: Readonly<{ [key: string]: string }>
+}>
+
+type PreloadImageFailure = Readonly<{
+ key: string
+ error: string
+}>
+
+type PreloadImagesResult = Readonly<{
+ succeeded: string[]
+ failed: PreloadImageFailure[]
+}>
+
+type WidgetServerCredentials = Readonly<{
+ token: string
+ headers?: Readonly<{ [key: string]: string }>
+}>
+
+export interface Spec extends TurboModule {
+ readonly onInteraction: CodegenTypes.EventEmitter
+ readonly onStateChanged: CodegenTypes.EventEmitter
+ readonly onActivityTokenReceived: CodegenTypes.EventEmitter
+ readonly onActivityPushToStartTokenReceived: CodegenTypes.EventEmitter
+ startLiveActivity(jsonString: string, options: StartVoltraOptions): Promise
+ updateLiveActivity(activityId: string, jsonString: string, options: UpdateVoltraOptions): Promise
+ endLiveActivity(activityId: string, options: EndVoltraOptions): Promise
+ endAllLiveActivities(): Promise
+ getLatestVoltraActivityId(): Promise
+ listVoltraActivityIds(): Promise
+ isLiveActivityActive(activityName: string): boolean
+ isHeadless(): boolean
+ preloadImages(images: PreloadImageOptions[]): Promise
+ reloadLiveActivities(activityNames?: string[] | null): Promise
+ clearPreloadedImages(keys?: string[] | null): Promise
+ updateWidget(widgetId: string, jsonString: string, options: UpdateWidgetOptions): Promise
+ scheduleWidget(widgetId: string, timelineJson: string): Promise
+ reloadWidgets(widgetIds?: string[] | null): Promise
+ clearWidget(widgetId: string): Promise
+ clearAllWidgets(): Promise
+ getActiveWidgets(): Promise
+ setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise
+ clearWidgetServerCredentials(): Promise
+}
+
+export function getNativeVoltra(): Spec {
+ const voltraModule = TurboModuleRegistry.get('NativeVoltra')
+
+ if (voltraModule == null) {
+ throw new Error('NativeVoltra is not available')
+ }
+
+ return voltraModule
+}
diff --git a/packages/ios-client/src/native/VoltraRNNativeComponent.ts b/packages/ios-client/src/native/VoltraRNNativeComponent.ts
new file mode 100644
index 00000000..a477b402
--- /dev/null
+++ b/packages/ios-client/src/native/VoltraRNNativeComponent.ts
@@ -0,0 +1,9 @@
+import type { HostComponent, ViewProps } from 'react-native'
+import { codegenNativeComponent } from 'react-native'
+
+export interface NativeProps extends ViewProps {
+ payload: string
+ viewId: string
+}
+
+export default codegenNativeComponent('VoltraView') as HostComponent
diff --git a/packages/ios-client/src/preload.ts b/packages/ios-client/src/preload.ts
index 1c2d4bca..418e90d1 100644
--- a/packages/ios-client/src/preload.ts
+++ b/packages/ios-client/src/preload.ts
@@ -1,7 +1,7 @@
import { Platform } from 'react-native'
import type { PreloadImageOptions, PreloadImagesResult } from './types.js'
-import VoltraModule from './VoltraModule.js'
+import { getNativeVoltra } from './VoltraModule.js'
export type { PreloadImageOptions, PreloadImagesResult } from './types.js'
@@ -17,7 +17,7 @@ export async function preloadImages(images: PreloadImageOptions[]): Promise {
if (!assertIOS('clearPreloadedImages')) return
try {
- await VoltraModule.clearPreloadedImages(keys ?? null)
+ await getNativeVoltra().clearPreloadedImages(keys ?? null)
} catch (error) {
console.error('Failed to clear preloaded images:', error)
}
diff --git a/packages/ios-client/src/widgets/server-credentials.ts b/packages/ios-client/src/widgets/server-credentials.ts
index 1120f2e2..b7c9670c 100644
--- a/packages/ios-client/src/widgets/server-credentials.ts
+++ b/packages/ios-client/src/widgets/server-credentials.ts
@@ -1,5 +1,5 @@
import type { WidgetServerCredentials } from '../types.js'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltra } from '../VoltraModule.js'
export type { WidgetServerCredentials } from '../types.js'
@@ -8,9 +8,9 @@ export async function setWidgetServerCredentials(credentials: WidgetServerCreden
throw new Error('[Voltra][iOS] setWidgetServerCredentials: token is required')
}
- return VoltraModule.setWidgetServerCredentials(credentials)
+ return getNativeVoltra().setWidgetServerCredentials(credentials)
}
export async function clearWidgetServerCredentials(): Promise {
- return VoltraModule.clearWidgetServerCredentials()
+ return getNativeVoltra().clearWidgetServerCredentials()
}
diff --git a/packages/ios-client/src/widgets/widget-api.ts b/packages/ios-client/src/widgets/widget-api.ts
index 775ba8bb..9da8b4a2 100644
--- a/packages/ios-client/src/widgets/widget-api.ts
+++ b/packages/ios-client/src/widgets/widget-api.ts
@@ -2,7 +2,7 @@ import { renderWidgetToString, type ScheduledWidgetEntry, type WidgetInfo, type
import type { UpdateWidgetOptions } from '../types.js'
import { assertRunningOnApple } from '../utils/assertRunningOnApple.js'
-import VoltraModule from '../VoltraModule.js'
+import { getNativeVoltra } from '../VoltraModule.js'
export type { UpdateWidgetOptions } from '../types.js'
export type { ScheduledWidgetEntry, WidgetInfo } from '@use-voltra/ios'
@@ -16,7 +16,7 @@ export const updateWidget = async (
const payload = renderWidgetToString(variants)
- return VoltraModule.updateWidget(widgetId, payload, {
+ return getNativeVoltra().updateWidget(widgetId, payload, {
deepLinkUrl: options?.deepLinkUrl,
})
}
@@ -24,19 +24,19 @@ export const updateWidget = async (
export const reloadWidgets = async (widgetIds?: string[]): Promise => {
if (!assertRunningOnApple()) return Promise.resolve()
- return VoltraModule.reloadWidgets(widgetIds ?? null)
+ return getNativeVoltra().reloadWidgets(widgetIds ?? null)
}
export const clearWidget = async (widgetId: string): Promise => {
if (!assertRunningOnApple()) return Promise.resolve()
- return VoltraModule.clearWidget(widgetId)
+ return getNativeVoltra().clearWidget(widgetId)
}
export const clearAllWidgets = async (): Promise => {
if (!assertRunningOnApple()) return Promise.resolve()
- return VoltraModule.clearAllWidgets()
+ return getNativeVoltra().clearAllWidgets()
}
export const scheduleWidget = async (widgetId: string, entries: ScheduledWidgetEntry[]): Promise => {
@@ -55,11 +55,11 @@ export const scheduleWidget = async (widgetId: string, entries: ScheduledWidgetE
const timelineJson = JSON.stringify(timelineData)
- return VoltraModule.scheduleWidget(widgetId, timelineJson)
+ return getNativeVoltra().scheduleWidget(widgetId, timelineJson)
}
export const getActiveWidgets = async (): Promise => {
if (!assertRunningOnApple()) return []
- return VoltraModule.getActiveWidgets()
+ return getNativeVoltra().getActiveWidgets()
}
diff --git a/packages/ios-client/tsconfig.build.json b/packages/ios-client/tsconfig.build.json
new file mode 100644
index 00000000..962cbcd4
--- /dev/null
+++ b/packages/ios-client/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.base.json",
+ "include": ["./src"],
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
+}
diff --git a/packages/ios-client/tsconfig.typecheck.json b/packages/ios-client/tsconfig.typecheck.json
index 477960f6..8526aab6 100644
--- a/packages/ios-client/tsconfig.typecheck.json
+++ b/packages/ios-client/tsconfig.typecheck.json
@@ -5,13 +5,12 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
"@use-voltra/android-server": ["packages/android-server/src/index.ts"],
"@use-voltra/core": ["packages/core/src/index.ts"],
"@use-voltra/ios": ["packages/ios/src/index.ts"],
+ "@use-voltra/ios-client/react-native": ["packages/ios-client/src/react-native/index.ts"],
"@use-voltra/ios/client": ["packages/ios-client/src/index.ts"],
"@use-voltra/ios-server": ["packages/ios-server/src/index.ts"],
"@use-voltra/server": ["packages/server/src/index.ts"]
diff --git a/packages/ios-server/README.md b/packages/ios-server/README.md
index df01adc6..2d293abf 100644
--- a/packages/ios-server/README.md
+++ b/packages/ios-server/README.md
@@ -7,13 +7,13 @@
`@use-voltra/ios-server` contains the iOS server rendering package for Voltra, including Live Activity rendering, widget rendering, and iOS widget update handlers.
> [!WARNING]
-> This package is not intended to be installed directly in your app. Most apps should install `voltra` instead.
+> Use in Node.js / backend only — not in React Native app bundles.
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
+Renders iOS Live Activity and widget JSX to payloads. See [Server-side updates](https://use-voltra.dev/ios/development/server-side-updates).
## 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.
@@ -22,9 +22,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-server?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-server?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/ios-server
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ./CONTRIBUTING.md
diff --git a/packages/ios-server/package.json b/packages/ios-server/package.json
index 0633c8c7..0cf84d8f 100644
--- a/packages/ios-server/package.json
+++ b/packages/ios-server/package.json
@@ -22,6 +22,7 @@
"build": "node ../../scripts/build-package.mjs packages/ios-server",
"clean": "rm -rf build",
"lint": "oxlint src",
+ "test": "node --test",
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
},
"dependencies": {
diff --git a/packages/ios-server/src/index.ts b/packages/ios-server/src/index.ts
index 796c1249..5a15ace5 100644
--- a/packages/ios-server/src/index.ts
+++ b/packages/ios-server/src/index.ts
@@ -4,7 +4,7 @@ import { promisify } from 'node:util'
import { brotliCompress, constants } from 'node:zlib'
import { type ComponentRegistry, createVoltraRenderer, ensurePayloadWithinBudget } from '@use-voltra/core'
-import type { LiveActivityVariants, WidgetVariants } from '@use-voltra/ios'
+import { getComponentId, type LiveActivityVariants, type WidgetVariants } from '@use-voltra/ios'
import type {
WidgetRenderRequest,
WidgetUpdateExpressHandler,
@@ -50,42 +50,8 @@ type LockScreenVariantObject = {
activityBackgroundTint?: string
}
-const COMPONENT_NAME_TO_ID: Record = {
- Text: 0,
- Button: 1,
- Label: 2,
- Image: 3,
- Symbol: 4,
- Toggle: 5,
- LinearProgressView: 6,
- CircularProgressView: 7,
- Gauge: 8,
- Timer: 9,
- LinearGradient: 10,
- VStack: 11,
- HStack: 12,
- ZStack: 13,
- GroupBox: 14,
- GlassContainer: 15,
- Spacer: 16,
- Divider: 17,
- Mask: 18,
- Link: 19,
- View: 20,
- Chart: 21,
-}
-
const defaultComponentRegistry: ComponentRegistry = {
- getComponentId: (name: string) => {
- const id = COMPONENT_NAME_TO_ID[name]
- if (id === undefined) {
- throw new Error(
- `Unknown component name: "${name}". Available components: ${Object.keys(COMPONENT_NAME_TO_ID).join(', ')}`
- )
- }
-
- return id
- },
+ getComponentId,
}
const brotliCompressAsync = promisify(brotliCompress)
diff --git a/packages/ios-server/test/index.test.js b/packages/ios-server/test/index.test.js
new file mode 100644
index 00000000..c562bdb6
--- /dev/null
+++ b/packages/ios-server/test/index.test.js
@@ -0,0 +1,191 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+
+const React = require('react')
+
+const {
+ Voltra,
+ createIOSWidgetUpdateExpressHandler,
+ createIOSWidgetUpdateHandler,
+ createIOSWidgetUpdateNodeHandler,
+ renderWidgetToString,
+} = require('../build/cjs/index.js')
+
+function createNodeResponseRecorder() {
+ return {
+ status: undefined,
+ headers: undefined,
+ body: undefined,
+ writeHead(status, headers) {
+ this.status = status
+ this.headers = headers
+ },
+ end(body) {
+ this.body = body
+ },
+ }
+}
+
+function createWidgetVariants(label) {
+ return {
+ systemSmall: React.createElement(Voltra.Text, null, label),
+ }
+}
+
+test('serializes iOS widget variants before returning the shared response', async () => {
+ const variants = createWidgetVariants('Hello from iOS')
+ const calls = []
+ const handler = createIOSWidgetUpdateHandler({
+ render(request) {
+ calls.push(request)
+ return variants
+ },
+ })
+
+ const response = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios&family=systemSmall&theme=dark', {
+ headers: {
+ authorization: 'Bearer ios-token',
+ 'x-voltra-request': 'ios-fetch',
+ },
+ })
+ )
+
+ assert.equal(response.status, 200)
+ assert.equal(await response.text(), renderWidgetToString(variants))
+ assert.equal(calls.length, 1)
+ assert.equal(calls[0].widgetId, 'weather')
+ assert.equal(calls[0].platform, 'ios')
+ assert.equal(calls[0].theme, 'dark')
+ assert.equal(calls[0].family, 'systemSmall')
+ assert.equal(calls[0].token, 'ios-token')
+ assert.equal(calls[0].headers.authorization, 'Bearer ios-token')
+ assert.equal(calls[0].headers['x-voltra-request'], 'ios-fetch')
+})
+
+test('passes validateToken through unchanged and preserves null render results', async () => {
+ const authCalls = []
+ const handler = createIOSWidgetUpdateHandler({
+ render() {
+ return null
+ },
+ validateToken(token) {
+ authCalls.push(token)
+ return token === 'valid-token'
+ },
+ })
+
+ const missingToken = await handler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(missingToken.status, 401)
+ assert.deepEqual(await missingToken.json(), { error: 'Authorization required' })
+ assert.deepEqual(authCalls, [])
+
+ const invalidToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios', {
+ headers: { authorization: 'Bearer nope' },
+ })
+ )
+ assert.equal(invalidToken.status, 401)
+ assert.deepEqual(await invalidToken.json(), { error: 'Invalid token' })
+
+ const validToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios', {
+ headers: { authorization: 'Bearer valid-token' },
+ })
+ )
+ assert.equal(validToken.status, 404)
+ assert.deepEqual(await validToken.json(), { error: 'No content for widget: weather' })
+ assert.deepEqual(authCalls, ['nope', 'valid-token'])
+})
+
+test('Node and Express adapters delegate through the same conversion path', async () => {
+ const variants = createWidgetVariants('Hello from adapter')
+ const calls = []
+ const options = {
+ render(request) {
+ calls.push({
+ widgetId: request.widgetId,
+ platform: request.platform,
+ theme: request.theme,
+ family: request.family,
+ token: request.token,
+ url: request.url.toString(),
+ headers: request.headers,
+ })
+ return variants
+ },
+ }
+
+ const nodeHandler = createIOSWidgetUpdateNodeHandler(options)
+ const nodeResponse = createNodeResponseRecorder()
+ await nodeHandler(
+ {
+ url: '/update?widgetId=weather&platform=ios&family=systemSmall',
+ method: 'POST',
+ headers: {
+ host: 'widgets.example.com',
+ authorization: 'Bearer node-token',
+ 'x-voltra-request': 'node',
+ },
+ socket: { encrypted: true },
+ },
+ nodeResponse
+ )
+
+ assert.equal(nodeResponse.status, 200)
+ assert.deepEqual(nodeResponse.headers, {
+ 'cache-control': 'no-cache',
+ 'content-type': 'application/json',
+ })
+ assert.equal(nodeResponse.body, renderWidgetToString(variants))
+
+ const expressHandler = createIOSWidgetUpdateExpressHandler(options)
+ const expressResponse = createNodeResponseRecorder()
+ await expressHandler(
+ {
+ url: '/update?widgetId=weather&platform=ios',
+ method: 'GET',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ socket: {},
+ },
+ expressResponse
+ )
+
+ assert.equal(expressResponse.status, 200)
+ assert.deepEqual(expressResponse.headers, {
+ 'cache-control': 'no-cache',
+ 'content-type': 'application/json',
+ })
+ assert.equal(expressResponse.body, renderWidgetToString(variants))
+
+ assert.deepEqual(calls, [
+ {
+ widgetId: 'weather',
+ platform: 'ios',
+ theme: 'light',
+ family: 'systemSmall',
+ token: 'node-token',
+ url: 'https://widgets.example.com/update?widgetId=weather&platform=ios&family=systemSmall',
+ headers: {
+ authorization: 'Bearer node-token',
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'node',
+ },
+ },
+ {
+ widgetId: 'weather',
+ platform: 'ios',
+ theme: 'light',
+ family: undefined,
+ token: undefined,
+ url: 'http://widgets.example.com/update?widgetId=weather&platform=ios',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ },
+ ])
+})
diff --git a/packages/ios-server/tsconfig.typecheck.json b/packages/ios-server/tsconfig.typecheck.json
index 83a4c4cc..fd44b930 100644
--- a/packages/ios-server/tsconfig.typecheck.json
+++ b/packages/ios-server/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/ios/README.md b/packages/ios/README.md
index a705e917..c6551290 100644
--- a/packages/ios/README.md
+++ b/packages/ios/README.md
@@ -1,19 +1,20 @@

-### Voltra for iOS
+### Voltra for iOS — JSX and rendering
[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
-`@use-voltra/ios` contains the iOS implementation package for Voltra, including the JSX namespace, Live Activities, and iOS widgets APIs.
+`@use-voltra/ios` contains the iOS JSX namespace (`Voltra`), payload rendering, and the `@use-voltra/ios/server` entry for Node.js.
-> [!WARNING]
-> This package is not intended to be installed directly in your app. Most apps should install `voltra` instead.
+**React Native apps** should install [`@use-voltra/ios-client`](../ios-client) only. The client package depends on this one and re-exports everything you need for app code, including `Voltra`.
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
+Use `@use-voltra/ios` directly when you need server rendering (`@use-voltra/ios-server` builds on top of it) or when working inside the monorepo.
+
+See [use-voltra.dev](https://use-voltra.dev/ios/setup) for app setup.
## 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.
@@ -22,9 +23,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?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/ios
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
-[prs-welcome]: ./CONTRIBUTING.md
+[prs-welcome]: ../../CONTRIBUTING.md
diff --git a/packages/ios/package.json b/packages/ios/package.json
index eda9ce92..9eec581e 100644
--- a/packages/ios/package.json
+++ b/packages/ios/package.json
@@ -2,21 +2,33 @@
"name": "@use-voltra/ios",
"version": "1.4.1",
"description": "Voltra for iOS",
- "main": "build/cjs/index.js",
- "module": "build/esm/index.js",
- "types": "build/types/index.d.ts",
+ "main": "build/commonjs/index.js",
+ "module": "build/module/index.js",
+ "types": "build/typescript/commonjs/index.d.ts",
"exports": {
".": {
- "types": "./build/types/index.d.ts",
- "require": "./build/cjs/index.js",
- "import": "./build/esm/index.js",
- "default": "./build/esm/index.js"
+ "source": "./src/index.ts",
+ "import": {
+ "types": "./build/typescript/module/index.d.ts",
+ "default": "./build/module/index.js"
+ },
+ "require": {
+ "types": "./build/typescript/commonjs/index.d.ts",
+ "default": "./build/commonjs/index.js"
+ },
+ "default": "./build/module/index.js"
},
"./server": {
- "types": "./build/types/server.d.ts",
- "require": "./build/cjs/server.js",
- "import": "./build/esm/server.js",
- "default": "./build/esm/server.js"
+ "source": "./src/server.ts",
+ "import": {
+ "types": "./build/typescript/module/server.d.ts",
+ "default": "./build/module/server.js"
+ },
+ "require": {
+ "types": "./build/typescript/commonjs/server.d.ts",
+ "default": "./build/commonjs/server.js"
+ },
+ "default": "./build/module/server.js"
},
"./package.json": "./package.json"
},
@@ -25,10 +37,11 @@
"README.md"
],
"scripts": {
- "build": "node ../../scripts/build-package.mjs packages/ios",
+ "build": "npm run clean && bob build",
"clean": "rm -rf build",
"lint": "oxlint src",
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
+ "test": "node --test"
},
"dependencies": {
"@use-voltra/core": "1.4.1"
@@ -53,5 +66,36 @@
"homepage": "https://use-voltra.dev",
"peerDependencies": {
"react": "*"
+ },
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "build",
+ "exclude": "**/{__tests__,__fixtures__,__mocks__,__rsc_tests__}/**",
+ "targets": [
+ [
+ "module",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "commonjs",
+ {
+ "esm": true,
+ "sourceMaps": true
+ }
+ ],
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ },
+ "devDependencies": {
+ "react-native-builder-bob": "^0.41.0",
+ "typescript": "~5.9.2"
}
}
diff --git a/packages/ios/src/index.ts b/packages/ios/src/index.ts
index e6a07b9d..24fe2343 100644
--- a/packages/ios/src/index.ts
+++ b/packages/ios/src/index.ts
@@ -1,4 +1,10 @@
export * as Voltra from './jsx/primitives.js'
+export {
+ getComponentId,
+ getComponentName,
+ COMPONENT_ID_TO_NAME,
+ COMPONENT_NAME_TO_ID,
+} from './payload/component-ids.js'
export { renderLiveActivityToJson, renderLiveActivityToString } from './live-activity/renderer.js'
export type {
DismissalPolicy,
diff --git a/packages/ios/src/jsx/createVoltraComponent.ts b/packages/ios/src/jsx/createVoltraComponent.ts
index 2b166b01..d3ff02ae 100644
--- a/packages/ios/src/jsx/createVoltraComponent.ts
+++ b/packages/ios/src/jsx/createVoltraComponent.ts
@@ -1,36 +1,2 @@
-import type { ComponentType } from 'react'
-import { createElement } from 'react'
-
-export const VOLTRA_COMPONENT_TAG = Symbol.for('VOLTRA_COMPONENT_TAG')
-
-export type VoltraComponent> = ComponentType & {
- displayName: string
- [VOLTRA_COMPONENT_TAG]: true
-}
-
-export type VoltraComponentOptions> = {
- toJSON?: (props: TProps) => Record
-}
-
-export const createVoltraComponent = >(
- componentName: string,
- options?: VoltraComponentOptions
-): VoltraComponent => {
- const Component = (props: TProps) => {
- const toJSON = options?.toJSON ? options.toJSON : (value: TProps) => value
- const normalizedProps = toJSON(props)
-
- return createElement(componentName, normalizedProps)
- }
-
- Component[VOLTRA_COMPONENT_TAG] = true
- Component.displayName = componentName
-
- return Component as VoltraComponent
-}
-
-export const isVoltraComponent = >(
- component: ComponentType
-): component is VoltraComponent => {
- return typeof component === 'function' && VOLTRA_COMPONENT_TAG in component
-}
+export { createVoltraComponent, isVoltraComponent, VOLTRA_COMPONENT_TAG } from '@use-voltra/core'
+export type { VoltraComponent, VoltraComponentOptions } from '@use-voltra/core'
diff --git a/packages/ios/src/jsx/props/.generated b/packages/ios/src/jsx/props/.generated
index 5f614e22..ff6735dd 100644
--- a/packages/ios/src/jsx/props/.generated
+++ b/packages/ios/src/jsx/props/.generated
@@ -1,4 +1,4 @@
-This directory contains auto-generated component props type files.
+This directory contains auto-generated iOS component props type files.
DO NOT EDIT MANUALLY.
Generated from: data/components.json
diff --git a/packages/ios/src/jsx/props/Box.ts b/packages/ios/src/jsx/props/Box.ts
deleted file mode 100644
index ab3b5ece..00000000
--- a/packages/ios/src/jsx/props/Box.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type BoxProps = VoltraBaseProps & {
- /** Spacing between children */
- spacing?: number
-}
diff --git a/packages/ios/src/jsx/props/FilledButton.ts b/packages/ios/src/jsx/props/FilledButton.ts
deleted file mode 100644
index 06e0cab9..00000000
--- a/packages/ios/src/jsx/props/FilledButton.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type FilledButtonProps = VoltraBaseProps & {
- /** Text to display */
- text: string
- /** Whether the button is enabled */
- enabled?: boolean
- /** Optional icon */
- icon?: Record
- /** Background color */
- backgroundColor?: string
- /** Text/icon color */
- contentColor?: string
- /** Maximum lines for text */
- maxLines?: number
-}
diff --git a/packages/ios/src/jsx/props/LegacyAndroidCheckBox.ts b/packages/ios/src/jsx/props/LegacyAndroidCheckBox.ts
deleted file mode 100644
index c6866a80..00000000
--- a/packages/ios/src/jsx/props/LegacyAndroidCheckBox.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type LegacyAndroidCheckBoxProps = VoltraBaseProps & {
- /** Unique identifier for this checkbox (required for interaction events) */
- id: string
- /** Initial checked state */
- checked?: boolean
-}
diff --git a/packages/ios/src/jsx/props/LegacyAndroidRadioButton.ts b/packages/ios/src/jsx/props/LegacyAndroidRadioButton.ts
deleted file mode 100644
index 6c6e2d0c..00000000
--- a/packages/ios/src/jsx/props/LegacyAndroidRadioButton.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type LegacyAndroidRadioButtonProps = VoltraBaseProps & {
- /** Unique identifier for this radio button (required for interaction events) */
- id: string
- /** Initial checked state */
- checked?: boolean
-}
diff --git a/packages/ios/src/jsx/props/LegacyAndroidSwitch.ts b/packages/ios/src/jsx/props/LegacyAndroidSwitch.ts
deleted file mode 100644
index 34221b9b..00000000
--- a/packages/ios/src/jsx/props/LegacyAndroidSwitch.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type LegacyAndroidSwitchProps = VoltraBaseProps & {
- /** Unique identifier for this switch (required for interaction events) */
- id: string
- /** Initial checked state */
- checked?: boolean
-}
diff --git a/packages/ios/src/jsx/props/LegacyFilledButton.ts b/packages/ios/src/jsx/props/LegacyFilledButton.ts
deleted file mode 100644
index c269f6c7..00000000
--- a/packages/ios/src/jsx/props/LegacyFilledButton.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type LegacyFilledButtonProps = VoltraBaseProps & {
- /** Text to display on the button */
- text: string
- /** Whether the button is enabled */
- enabled?: boolean
-}
diff --git a/packages/ios/src/jsx/props/LegacyImage.ts b/packages/ios/src/jsx/props/LegacyImage.ts
deleted file mode 100644
index a5577ea4..00000000
--- a/packages/ios/src/jsx/props/LegacyImage.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// 🤖 AUTO-GENERATED from data/components.json
-// DO NOT EDIT MANUALLY - Changes will be overwritten
-// Schema version: 1.0.0
-
-import type { VoltraBaseProps } from '../baseProps'
-
-export type LegacyImageProps = VoltraBaseProps & {
- /** Image source - { assetName: string } for Android drawable resources or preloaded assets */
- source: Record
- /** How the image should be resized to fit its container */
- resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center'
-}
diff --git a/packages/ios/src/renderer/context-registry.ts b/packages/ios/src/renderer/context-registry.ts
deleted file mode 100644
index 4d2f7f38..00000000
--- a/packages/ios/src/renderer/context-registry.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { type ContextRegistry, getContextRegistry } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/dispatcher.ts b/packages/ios/src/renderer/dispatcher.ts
deleted file mode 100644
index 8b226bdb..00000000
--- a/packages/ios/src/renderer/dispatcher.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { getHooksDispatcher, getReactCurrentDispatcher } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/element-registry.ts b/packages/ios/src/renderer/element-registry.ts
deleted file mode 100644
index bf7eac22..00000000
--- a/packages/ios/src/renderer/element-registry.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { createElementRegistry, type ElementRegistry, preScanForDuplicates } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/flatten-styles.ts b/packages/ios/src/renderer/flatten-styles.ts
deleted file mode 100644
index eca7be55..00000000
--- a/packages/ios/src/renderer/flatten-styles.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { flattenStyle } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/index.ts b/packages/ios/src/renderer/index.ts
index 16c35868..a1f0f9b4 100644
--- a/packages/ios/src/renderer/index.ts
+++ b/packages/ios/src/renderer/index.ts
@@ -1,4 +1,3 @@
-export { flattenStyle } from './flatten-styles.js'
export {
type ComponentRegistry,
createVoltraRenderer,
diff --git a/packages/ios/src/renderer/render-cache.ts b/packages/ios/src/renderer/render-cache.ts
deleted file mode 100644
index 5463b4d2..00000000
--- a/packages/ios/src/renderer/render-cache.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { getRenderCache, type RenderCache } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/stylesheet-registry.ts b/packages/ios/src/renderer/stylesheet-registry.ts
deleted file mode 100644
index d5c6a462..00000000
--- a/packages/ios/src/renderer/stylesheet-registry.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { createStylesheetRegistry, type StylesheetRegistry } from '@use-voltra/core'
diff --git a/packages/ios/src/renderer/types.ts b/packages/ios/src/renderer/types.ts
deleted file mode 100644
index 1f044692..00000000
--- a/packages/ios/src/renderer/types.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type { VoltraVariantRenderer } from '@use-voltra/core'
diff --git a/packages/ios/test/renderer.test.js b/packages/ios/test/renderer.test.js
new file mode 100644
index 00000000..621ac2bd
--- /dev/null
+++ b/packages/ios/test/renderer.test.js
@@ -0,0 +1,213 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const React = require('react')
+
+const ios = require('../build/commonjs/index.js')
+const { createVoltraComponent } = require('../build/commonjs/jsx/createVoltraComponent.js')
+
+const {
+ COMPONENT_NAME_TO_ID,
+ Voltra,
+ getComponentId,
+ renderLiveActivityToJson,
+ renderLiveActivityToString,
+ renderVoltraVariantToJson,
+ renderWidgetToJson,
+ renderWidgetToString,
+} = ios
+
+test('renders widget families into the expected payload roots', () => {
+ const widgetJson = renderWidgetToJson({
+ systemSmall: React.createElement(Voltra.Text, null, 'Small'),
+ systemMedium: React.createElement(
+ Voltra.View,
+ { testID: 'medium-root' },
+ React.createElement(Voltra.Text, null, 'Medium')
+ ),
+ systemLarge: React.createElement(Voltra.VStack, null, React.createElement(Voltra.Text, null, 'Large')),
+ accessoryCircular: React.createElement(Voltra.Text, null, 'Circle'),
+ accessoryInline: null,
+ accessoryRectangular: undefined,
+ })
+
+ assert.deepEqual(widgetJson, {
+ v: 1,
+ systemSmall: {
+ t: getComponentId('Text'),
+ c: 'Small',
+ },
+ systemMedium: {
+ t: getComponentId('View'),
+ c: {
+ t: getComponentId('Text'),
+ c: 'Medium',
+ },
+ p: {
+ testID: 'medium-root',
+ },
+ },
+ systemLarge: {
+ t: getComponentId('VStack'),
+ c: {
+ t: getComponentId('Text'),
+ c: 'Large',
+ },
+ },
+ accessoryCircular: {
+ t: getComponentId('Text'),
+ c: 'Circle',
+ },
+ })
+
+ assert.equal(
+ renderWidgetToString({ systemSmall: React.createElement(Voltra.Text, null, 'Small') }),
+ JSON.stringify({
+ v: 1,
+ systemSmall: {
+ t: getComponentId('Text'),
+ c: 'Small',
+ },
+ })
+ )
+ assert.ok(!('accessoryInline' in widgetJson))
+ assert.ok(!('accessoryRectangular' in widgetJson))
+})
+
+test('renders live activity slots and metadata into the expected keys', () => {
+ const liveActivityJson = renderLiveActivityToJson({
+ lockScreen: {
+ content: React.createElement(Voltra.Text, null, 'Lock screen'),
+ activityBackgroundTint: '#112233',
+ },
+ island: {
+ expanded: {
+ center: React.createElement(Voltra.View, null, React.createElement(Voltra.Text, null, 'Center')),
+ leading: React.createElement(Voltra.Text, null, 'Leading'),
+ trailing: React.createElement(Voltra.Text, null, 'Trailing'),
+ bottom: React.createElement(Voltra.Text, null, 'Bottom'),
+ },
+ compact: {
+ leading: React.createElement(Voltra.Text, null, 'Compact leading'),
+ trailing: React.createElement(Voltra.Text, null, 'Compact trailing'),
+ },
+ minimal: React.createElement(Voltra.Text, null, 'Minimal'),
+ keylineTint: '#445566',
+ },
+ supplementalActivityFamilies: {
+ small: React.createElement(Voltra.Text, null, 'Supplemental'),
+ },
+ })
+
+ assert.deepEqual(liveActivityJson, {
+ v: 1,
+ ls: {
+ t: getComponentId('Text'),
+ c: 'Lock screen',
+ },
+ isl_exp_c: {
+ t: getComponentId('View'),
+ c: {
+ t: getComponentId('Text'),
+ c: 'Center',
+ },
+ },
+ isl_exp_l: {
+ t: getComponentId('Text'),
+ c: 'Leading',
+ },
+ isl_exp_t: {
+ t: getComponentId('Text'),
+ c: 'Trailing',
+ },
+ isl_exp_b: {
+ t: getComponentId('Text'),
+ c: 'Bottom',
+ },
+ isl_cmp_l: {
+ t: getComponentId('Text'),
+ c: 'Compact leading',
+ },
+ isl_cmp_t: {
+ t: getComponentId('Text'),
+ c: 'Compact trailing',
+ },
+ isl_min: {
+ t: getComponentId('Text'),
+ c: 'Minimal',
+ },
+ saf_sm: {
+ t: getComponentId('Text'),
+ c: 'Supplemental',
+ },
+ ls_background_tint: '#112233',
+ isl_keyline_tint: '#445566',
+ })
+
+ assert.equal(
+ renderLiveActivityToString({
+ lockScreen: React.createElement(Voltra.Text, null, 'Lock screen'),
+ island: {
+ minimal: React.createElement(Voltra.Text, null, 'Minimal'),
+ },
+ }),
+ JSON.stringify({
+ v: 1,
+ ls: {
+ t: getComponentId('Text'),
+ c: 'Lock screen',
+ },
+ isl_min: {
+ t: getComponentId('Text'),
+ c: 'Minimal',
+ },
+ })
+ )
+})
+
+test('omits absent live activity roots and metadata', () => {
+ const liveActivityJson = renderLiveActivityToJson({
+ lockScreen: {
+ content: null,
+ },
+ island: {
+ expanded: {
+ leading: undefined,
+ },
+ compact: {
+ trailing: null,
+ },
+ minimal: undefined,
+ },
+ supplementalActivityFamilies: {
+ small: null,
+ },
+ })
+
+ assert.deepEqual(liveActivityJson, {
+ v: 1,
+ })
+})
+
+test('uses the generated component registry for known components', () => {
+ assert.equal(COMPONENT_NAME_TO_ID.Text, getComponentId('Text'))
+ assert.equal(COMPONENT_NAME_TO_ID.View, getComponentId('View'))
+
+ assert.deepEqual(
+ renderVoltraVariantToJson(React.createElement(Voltra.View, null, React.createElement(Voltra.Text, null, 'Hello'))),
+ {
+ t: getComponentId('View'),
+ c: {
+ t: getComponentId('Text'),
+ c: 'Hello',
+ },
+ }
+ )
+})
+
+test('fails loudly for unknown generated component names', () => {
+ const Unknown = createVoltraComponent('UnknownWidget')
+
+ assert.throws(() => renderVoltraVariantToJson(React.createElement(Unknown, null)), {
+ message: /Unknown component name: "UnknownWidget"/,
+ })
+})
diff --git a/packages/ios/tsconfig.base.json b/packages/ios/tsconfig.base.json
index 49f9c30e..4833c865 100644
--- a/packages/ios/tsconfig.base.json
+++ b/packages/ios/tsconfig.base.json
@@ -1,16 +1,12 @@
{
+ "extends": "expo-module-scripts/tsconfig.base",
"compilerOptions": {
- "target": "ES2020",
- "lib": ["ES2020"],
"rootDir": "./src",
+ "baseUrl": ".",
"moduleResolution": "node",
- "jsx": "react-jsx",
- "strict": true,
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "skipLibCheck": true,
- "resolveJsonModule": true,
- "isolatedModules": true
+ "paths": {
+ "@use-voltra/core": ["../core/build/types/index.d.ts"]
+ }
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
diff --git a/packages/ios/tsconfig.build.json b/packages/ios/tsconfig.build.json
new file mode 100644
index 00000000..962cbcd4
--- /dev/null
+++ b/packages/ios/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.base.json",
+ "include": ["./src"],
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
+}
diff --git a/packages/ios/tsconfig.cjs.json b/packages/ios/tsconfig.cjs.json
deleted file mode 100644
index a6b3ca9c..00000000
--- a/packages/ios/tsconfig.cjs.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "extends": "./tsconfig.base.json",
- "compilerOptions": {
- "module": "CommonJS",
- "outDir": "./build/cjs",
- "declaration": false,
- "sourceMap": true
- }
-}
diff --git a/packages/ios/tsconfig.esm.json b/packages/ios/tsconfig.esm.json
deleted file mode 100644
index 2bb18d33..00000000
--- a/packages/ios/tsconfig.esm.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "extends": "./tsconfig.base.json",
- "compilerOptions": {
- "module": "ES2020",
- "outDir": "./build/esm",
- "declaration": false,
- "sourceMap": true
- }
-}
diff --git a/packages/ios/tsconfig.typecheck.json b/packages/ios/tsconfig.typecheck.json
index 83a4c4cc..fd44b930 100644
--- a/packages/ios/tsconfig.typecheck.json
+++ b/packages/ios/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/ios/tsconfig.types.json b/packages/ios/tsconfig.types.json
deleted file mode 100644
index 8ec821ee..00000000
--- a/packages/ios/tsconfig.types.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "./tsconfig.base.json",
- "compilerOptions": {
- "module": "ES2020",
- "outDir": "./build/types",
- "declaration": true,
- "emitDeclarationOnly": true,
- "declarationMap": true
- }
-}
diff --git a/packages/server/README.md b/packages/server/README.md
index 8c87c3cb..d62bb7b0 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -7,13 +7,15 @@
`@use-voltra/server` contains the shared server rendering foundation for Voltra, including request adapters and platform-agnostic widget update handlers.
> [!WARNING]
-> This package is not intended to be installed directly in your app. Most apps should install `voltra` instead.
+> Use in Node.js / backend only — not in React Native app bundles.
-For installation and setup instructions, see the Voltra documentation: [use-voltra.dev](https://use-voltra.dev).
+Shared HTTP handlers for server-driven widgets (`createWidgetUpdateNodeHandler`, etc.). Pair with `@use-voltra/ios-server` and/or `@use-voltra/android-server` for JSX rendering.
+
+See [use-voltra.dev](https://use-voltra.dev).
## 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.
@@ -22,9 +24,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/server?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/server?style=for-the-badge
+[npm-downloads]: https://www.npmjs.com/package/@use-voltra/server
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ./CONTRIBUTING.md
diff --git a/packages/server/package.json b/packages/server/package.json
index 0b744dfa..c38fb865 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -22,6 +22,7 @@
"build": "node ../../scripts/build-package.mjs packages/server",
"clean": "rm -rf build",
"lint": "oxlint src",
+ "test": "node --test",
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
},
"keywords": [
diff --git a/packages/server/test/index.test.js b/packages/server/test/index.test.js
new file mode 100644
index 00000000..9d11e9bb
--- /dev/null
+++ b/packages/server/test/index.test.js
@@ -0,0 +1,273 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+
+const {
+ createWidgetUpdateExpressHandler,
+ createWidgetUpdateHandler,
+ createWidgetUpdateNodeHandler,
+} = require('../build/cjs/index.js')
+
+function createNodeResponseRecorder() {
+ return {
+ status: undefined,
+ headers: undefined,
+ body: undefined,
+ writeHead(status, headers) {
+ this.status = status
+ this.headers = headers
+ },
+ end(body) {
+ this.body = body
+ },
+ }
+}
+
+test('validates required query parameters', async () => {
+ const handler = createWidgetUpdateHandler({})
+
+ const missingWidgetId = await handler(new Request('https://example.com/update?platform=ios'))
+ assert.equal(missingWidgetId.status, 400)
+ assert.deepEqual(await missingWidgetId.json(), {
+ error: 'Missing required query parameter: widgetId',
+ })
+
+ const missingPlatform = await handler(new Request('https://example.com/update?widgetId=weather'))
+ assert.equal(missingPlatform.status, 400)
+ assert.deepEqual(await missingPlatform.json(), {
+ error: 'Missing or invalid required query parameter: platform',
+ })
+
+ const invalidPlatform = await handler(new Request('https://example.com/update?widgetId=weather&platform=web'))
+ assert.equal(invalidPlatform.status, 400)
+ assert.deepEqual(await invalidPlatform.json(), {
+ error: 'Missing or invalid required query parameter: platform',
+ })
+})
+
+test('enforces bearer token validation only when configured', async () => {
+ const authCalls = []
+ const handler = createWidgetUpdateHandler({
+ renderIos: ({ token }) => JSON.stringify({ ok: true, token }),
+ validateToken(token) {
+ authCalls.push(token)
+ return token === 'valid-token'
+ },
+ })
+
+ const missingToken = await handler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(missingToken.status, 401)
+ assert.deepEqual(await missingToken.json(), { error: 'Authorization required' })
+ assert.deepEqual(authCalls, [])
+
+ const invalidToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios', {
+ headers: { authorization: 'Bearer nope' },
+ })
+ )
+ assert.equal(invalidToken.status, 401)
+ assert.deepEqual(await invalidToken.json(), { error: 'Invalid token' })
+ assert.deepEqual(authCalls, ['nope'])
+
+ const validToken = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios', {
+ headers: { authorization: 'Bearer valid-token' },
+ })
+ )
+ assert.equal(validToken.status, 200)
+ assert.deepEqual(await validToken.json(), { ok: true, token: 'valid-token' })
+ assert.deepEqual(authCalls, ['nope', 'valid-token'])
+
+ const noAuthHandler = createWidgetUpdateHandler({
+ renderIos: ({ token }) => JSON.stringify({ ok: true, token: token ?? null }),
+ })
+ const noAuthResponse = await noAuthHandler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(noAuthResponse.status, 200)
+ assert.deepEqual(await noAuthResponse.json(), { ok: true, token: null })
+})
+
+test('parses request context and routes to platform renderers', async () => {
+ const iosCalls = []
+ const androidCalls = []
+ const handler = createWidgetUpdateHandler({
+ renderIos(request) {
+ iosCalls.push(request)
+ return JSON.stringify({ platform: request.platform, widgetId: request.widgetId })
+ },
+ renderAndroid(request) {
+ androidCalls.push(request)
+ return JSON.stringify({ platform: request.platform, widgetId: request.widgetId })
+ },
+ })
+
+ const iosResponse = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios&theme=dark&family=systemSmall', {
+ headers: {
+ authorization: 'Bearer ios-token',
+ 'x-voltra-request': 'ios',
+ },
+ })
+ )
+ assert.equal(iosResponse.status, 200)
+ assert.deepEqual(await iosResponse.json(), { platform: 'ios', widgetId: 'weather' })
+ assert.equal(iosCalls.length, 1)
+ assert.equal(androidCalls.length, 0)
+ assert.equal(iosCalls[0].widgetId, 'weather')
+ assert.equal(iosCalls[0].platform, 'ios')
+ assert.equal(iosCalls[0].theme, 'dark')
+ assert.equal(iosCalls[0].family, 'systemSmall')
+ assert.equal(iosCalls[0].token, 'ios-token')
+ assert.equal(iosCalls[0].url.searchParams.get('widgetId'), 'weather')
+ assert.equal(iosCalls[0].headers.authorization, 'Bearer ios-token')
+ assert.equal(iosCalls[0].headers['x-voltra-request'], 'ios')
+
+ const androidResponse = await handler(
+ new Request('https://example.com/update?widgetId=weather&platform=android', {
+ headers: { 'x-voltra-request': 'android' },
+ })
+ )
+ assert.equal(androidResponse.status, 200)
+ assert.deepEqual(await androidResponse.json(), { platform: 'android', widgetId: 'weather' })
+ assert.equal(androidCalls.length, 1)
+ assert.equal(androidCalls[0].theme, 'light')
+ assert.equal(androidCalls[0].family, undefined)
+ assert.equal(androidCalls[0].token, undefined)
+ assert.equal(androidCalls[0].headers['x-voltra-request'], 'android')
+})
+
+test('returns stable 404 and 200 responses for renderer outcomes', async () => {
+ const missingIosHandler = createWidgetUpdateHandler({ renderAndroid: () => '{"ok":true}' })
+ const missingIosRenderer = await missingIosHandler(
+ new Request('https://example.com/update?widgetId=weather&platform=ios')
+ )
+ assert.equal(missingIosRenderer.status, 404)
+ assert.deepEqual(await missingIosRenderer.json(), {
+ error: 'No iOS render handler configured for widget: weather',
+ })
+
+ const nullIosHandler = createWidgetUpdateHandler({ renderIos: () => null })
+ const nullIosResponse = await nullIosHandler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(nullIosResponse.status, 404)
+ assert.deepEqual(await nullIosResponse.json(), {
+ error: 'No content for widget: weather',
+ })
+
+ const nullAndroidHandler = createWidgetUpdateHandler({ renderAndroid: () => null })
+ const nullAndroidResponse = await nullAndroidHandler(
+ new Request('https://example.com/update?widgetId=weather&platform=android')
+ )
+ assert.equal(nullAndroidResponse.status, 404)
+ assert.deepEqual(await nullAndroidResponse.json(), {
+ error: 'No content for Android widget: weather',
+ })
+
+ const successHandler = createWidgetUpdateHandler({ renderIos: () => '{"message":"ok"}' })
+ const successResponse = await successHandler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(successResponse.status, 200)
+ assert.equal(successResponse.headers.get('content-type'), 'application/json')
+ assert.equal(successResponse.headers.get('cache-control'), 'no-cache')
+ assert.equal(await successResponse.text(), '{"message":"ok"}')
+})
+
+test('returns a stable 500 response when renderers throw', async () => {
+ const originalConsoleError = console.error
+ console.error = () => {}
+
+ try {
+ const handler = createWidgetUpdateHandler({
+ renderIos() {
+ throw new Error('boom')
+ },
+ })
+
+ const response = await handler(new Request('https://example.com/update?widgetId=weather&platform=ios'))
+ assert.equal(response.status, 500)
+ assert.deepEqual(await response.json(), { error: 'Internal server error' })
+ assert.equal(response.headers.get('content-type'), 'application/json')
+ } finally {
+ console.error = originalConsoleError
+ }
+})
+
+test('adapts Node and Express-style requests consistently', async () => {
+ const calls = []
+ const options = {
+ renderAndroid(request) {
+ calls.push(request)
+ return JSON.stringify({
+ widgetId: request.widgetId,
+ platform: request.platform,
+ theme: request.theme,
+ family: request.family,
+ token: request.token,
+ url: request.url.toString(),
+ headers: request.headers,
+ })
+ },
+ }
+
+ const nodeHandler = createWidgetUpdateNodeHandler(options)
+ const nodeResponse = createNodeResponseRecorder()
+ await nodeHandler(
+ {
+ url: '/update?widgetId=weather&platform=android&family=2x2',
+ method: 'POST',
+ headers: {
+ host: 'widgets.example.com',
+ authorization: 'Bearer node-token',
+ 'x-voltra-request': 'node',
+ 'x-many': ['one', 'two'],
+ },
+ socket: { encrypted: true },
+ },
+ nodeResponse
+ )
+
+ assert.equal(nodeResponse.status, 200)
+ assert.deepEqual(nodeResponse.headers, {
+ 'cache-control': 'no-cache',
+ 'content-type': 'application/json',
+ })
+ assert.deepEqual(JSON.parse(nodeResponse.body), {
+ widgetId: 'weather',
+ platform: 'android',
+ theme: 'light',
+ family: '2x2',
+ token: 'node-token',
+ url: 'https://widgets.example.com/update?widgetId=weather&platform=android&family=2x2',
+ headers: {
+ authorization: 'Bearer node-token',
+ host: 'widgets.example.com',
+ 'x-many': 'one, two',
+ 'x-voltra-request': 'node',
+ },
+ })
+
+ const expressHandler = createWidgetUpdateExpressHandler(options)
+ const expressResponse = createNodeResponseRecorder()
+ await expressHandler(
+ {
+ url: '/update?widgetId=weather&platform=android',
+ method: 'GET',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ socket: {},
+ },
+ expressResponse
+ )
+
+ assert.equal(expressResponse.status, 200)
+ assert.deepEqual(JSON.parse(expressResponse.body), {
+ widgetId: 'weather',
+ platform: 'android',
+ theme: 'light',
+ url: 'http://widgets.example.com/update?widgetId=weather&platform=android',
+ headers: {
+ host: 'widgets.example.com',
+ 'x-voltra-request': 'express',
+ },
+ })
+
+ assert.equal(calls.length, 2)
+})
diff --git a/packages/server/tsconfig.typecheck.json b/packages/server/tsconfig.typecheck.json
index 83a4c4cc..fd44b930 100644
--- a/packages/server/tsconfig.typecheck.json
+++ b/packages/server/tsconfig.typecheck.json
@@ -5,8 +5,6 @@
"rootDir": "../..",
"baseUrl": "../..",
"paths": {
- "voltra": ["packages/voltra/src/index.ts"],
- "voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android-client": ["packages/android-client/src/index.ts"],
"@use-voltra/android/client": ["packages/android-client/src/index.ts"],
diff --git a/packages/voltra/.npmignore b/packages/voltra/.npmignore
deleted file mode 100644
index 14a0caf6..00000000
--- a/packages/voltra/.npmignore
+++ /dev/null
@@ -1,38 +0,0 @@
-# Exclude top-level hidden files and directories generated locally
-/.*
-
-# Exclude tarballs generated by `npm pack`
-/*.tgz
-
-# Tests
-__mocks__/
-__tests__/
-
-# Development and build configuration
-/babel.config.js
-/jest.config.js
-/tsconfig*.json
-/.nvmrc
-/.prettierignore
-/.prettierrc
-/.swiftformat
-/.editorconfig
-
-# Android build artifacts
-/android/.gradle/
-/android/src/androidTest/
-/android/src/test/
-/android/build/
-
-# Swift test infrastructure (package-only)
-/ios/Package.swift
-/ios/Tests/
-/ios/.build/
-
-# Source files (we publish the built version)
-/src/
-
-# Development tools and generators
-/generator/
-/schemas/
-/data/
diff --git a/packages/voltra/CHANGELOG.md b/packages/voltra/CHANGELOG.md
deleted file mode 100644
index 80da5725..00000000
--- a/packages/voltra/CHANGELOG.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# voltra
-
-## 1.4.1
-
-### Patch Changes
-
-- a5a315b: Fix `maxLines` text truncation on Android widgets so line limits apply correctly.
-- iOS home screen widgets now match Tinted and Clear system appearances: no more default opaque white card behind your widget, with colors and gradients adjusted so content stays readable.
-- Updated dependencies [a5a315b]
-- Updated dependencies
- - @use-voltra/android@1.4.1
- - @use-voltra/android-client@1.4.1
- - @use-voltra/android-server@1.4.1
- - @use-voltra/core@1.4.1
- - @use-voltra/expo-plugin@1.4.1
- - @use-voltra/ios@1.4.1
- - @use-voltra/ios-client@1.4.1
- - @use-voltra/ios-server@1.4.1
- - @use-voltra/server@1.4.1
-
-## 1.4.0
-
-### Minor Changes
-
-- Android home-screen widgets can use colors that follow the user’s theme and wallpaper (including Material You), so widgets feel native in light, dark, and dynamic setups. If you drive widgets from your own server, you can read the full request URL—including query parameters—when handling updates, which makes it easier to personalize or A/B content per link. Widget updates on iOS are a bit more forgiving when variant data is missing.
-- 14d4fa5: Add Android ongoing notification support, including richer notification content, remote update flows, and server-side payload rendering APIs. This release also expands the Expo integration and documentation so apps can configure, send, and manage Android ongoing notifications more easily.
-
-### Patch Changes
-
-- Android apps built for production (minified / release) are less likely to crash or mis-render widgets because of how widget payloads are processed on the device.
-- 8cedb47: Fix iOS Live Activity naming so named activities can be reused more reliably across app launches.
-- Work on decomposing Voltra into smaller packages continues, and more pieces have moved from the umbrella package into the respective `@use-voltra/*` packages. You should still use the `voltra` umbrella for your app.
-- Updated dependencies
-- Updated dependencies [14d4fa5]
-- Updated dependencies
- - @use-voltra/android@1.4.0
- - @use-voltra/android-server@1.4.0
- - @use-voltra/ios-server@1.3.2
- - @use-voltra/server@1.4.0
- - @use-voltra/expo-plugin@1.4.0
- - @use-voltra/android-client@1.4.0
- - @use-voltra/core@1.4.0
- - @use-voltra/ios@1.4.0
- - @use-voltra/ios-client@1.4.0
-
-## 1.3.0
-
-### Patch Changes
-
-- Updated dependencies [2585b90]
-- Updated dependencies [27e3db1]
-- Updated dependencies [68271bb]
-- Updated dependencies [64a7f4b]
-- Updated dependencies [672d91f]
-- Updated dependencies [0d30973]
-- Updated dependencies [b1efcad]
- - @use-voltra/android@1.3.0
- - @use-voltra/ios@1.3.0
- - @use-voltra/expo-plugin@1.3.0
- - @use-voltra/ios-server@1.3.0
- - @use-voltra/android-server@1.3.0
- - @use-voltra/server@1.3.0
diff --git a/packages/voltra/README.md b/packages/voltra/README.md
deleted file mode 100644
index b7692cad..00000000
--- a/packages/voltra/README.md
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-### Build Live Activities and Widgets with JSX in React Native
-
-[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
-
-Voltra turns React Native JSX into SwiftUI and Jetpack Compose Glance so you can ship custom Live Activities, Dynamic Island layouts, and Android Widgets without touching native code. Author everything in React, keep hot reload, and let the config plugin handle the native extension targets.
-
-## Features
-
-- **Ship Native Surfaces**: Create iOS Live Activities, Dynamic Island variants, and Android Home Screen widgets directly from React components - no Swift, Kotlin, or Xcode/Android Studio UI work required.
-
-- **Fast Development Workflow**: Hooks respect Fast Refresh and both JS and native layers enforce platform-specific payload budgets.
-
-- **Production-Ready Push Notifications**: Support for ActivityKit push tokens (iOS) and FCM (Android) to stream lifecycle updates and build server-driven refreshes.
-
-- **Familiar Styling**: Use React Native style props and platform-native modifiers (SwiftUI/Glance) in one place.
-
-- **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.
-
-## Documentation
-
-The documentation is available at [use-voltra.dev](https://use-voltra.dev). You can also use the following links to jump to specific topics:
-
-- [Getting Started](https://use-voltra.dev/getting-started/introduction)
-- [Installation](https://use-voltra.dev/getting-started/installation)
-- [iOS Setup](https://use-voltra.dev/ios/setup)
-- [Android Setup](https://use-voltra.dev/android/setup)
-- [iOS Development](https://use-voltra.dev/ios/development/developing-live-activities)
-- [Android Development](https://use-voltra.dev/android/development/developing-widgets)
-- [iOS API Reference](https://use-voltra.dev/ios/api/configuration)
-- [Android API Reference](https://use-voltra.dev/android/api/plugin-configuration)
-
-## Getting started
-
-> [!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:
-
-```sh
-npm install voltra
-```
-
-Add the config plugin to your `app.json`:
-
-```json
-{
- "expo": {
- "plugins": ["voltra"]
- }
-}
-```
-
-Then run `npx expo prebuild --clean` to generate the native extension targets.
-
-See the [documentation](https://use-voltra.dev/getting-started/installation) for detailed setup instructions.
-
-## Quick example
-
-```tsx
-import { useLiveActivity } from 'voltra/client'
-import { Voltra } from 'voltra'
-
-export function OrderTracker({ orderId }: { orderId: string }) {
- const ui = (
-
- Order #{orderId}
- Driver en route · ETA 12 min
-
- )
-
- const { start, update, end } = useLiveActivity(
- { lockScreen: ui },
- {
- activityName: `order-${orderId}`,
- autoStart: true,
- deepLinkUrl: `/orders/${orderId}`,
- }
- )
-
- return null
-}
-```
-
-## Platform compatibility
-
-Voltra is a cross-platform library that supports:
-
-- **iOS**: Live Activities and Dynamic Island (SwiftUI).
-- **Android**: Home Screen Widgets (Jetpack Compose Glance).
-
-## 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].
-
-If you think it's cool, please star it 🌟. This project will always remain free to use.
-
-[Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
-
-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]: 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
-[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
-[prs-welcome]: ./CONTRIBUTING.md
diff --git a/packages/voltra/android/src/main/java/voltra/VoltraModule.kt b/packages/voltra/android/src/main/java/voltra/VoltraModule.kt
deleted file mode 100644
index fb7c1e83..00000000
--- a/packages/voltra/android/src/main/java/voltra/VoltraModule.kt
+++ /dev/null
@@ -1,489 +0,0 @@
-package voltra
-
-import android.appwidget.AppWidgetManager
-import android.util.Log
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import androidx.glance.appwidget.GlanceAppWidgetManager
-import expo.modules.kotlin.modules.Module
-import expo.modules.kotlin.modules.ModuleDefinition
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.runBlocking
-import voltra.events.VoltraEventBus
-import voltra.images.VoltraImageManager
-import voltra.widget.VoltraGlanceWidget
-import voltra.widget.VoltraWidgetManager
-
-class VoltraModule : Module() {
- companion object {
- private const val TAG = "VoltraModule"
- }
-
- private val notificationManager by lazy {
- VoltraNotificationManager(appContext.reactContext!!)
- }
-
- private val widgetManager by lazy {
- VoltraWidgetManager(appContext.reactContext!!)
- }
-
- private val imageManager by lazy {
- VoltraImageManager(appContext.reactContext!!)
- }
-
- private val eventBus by lazy {
- VoltraEventBus.getInstance(appContext.reactContext!!)
- }
-
- private var eventBusUnsubscribe: (() -> Unit)? = null
-
- override fun definition() =
- ModuleDefinition {
- Name("VoltraModule")
-
- OnStartObserving {
- Log.d(TAG, "OnStartObserving: Starting event bus subscription")
-
- // Replay any persisted events from SharedPreferences (cold start)
- val persistedEvents = eventBus.popAll()
- Log.d(TAG, "Replaying ${persistedEvents.size} persisted events")
-
- persistedEvents.forEach { event ->
- sendEvent(event.type, event.toMap())
- }
-
- // Subscribe to hot event delivery (broadcast receiver)
- eventBusUnsubscribe =
- eventBus.addListener { event ->
- Log.d(TAG, "Received hot event: ${event.type}")
- sendEvent(event.type, event.toMap())
- }
- }
-
- OnStopObserving {
- Log.d(TAG, "OnStopObserving: Unsubscribing from event bus")
- eventBusUnsubscribe?.invoke()
- eventBusUnsubscribe = null
- }
-
- // Android ongoing notification APIs
-
- AsyncFunction("startAndroidOngoingNotification") {
- payload: String,
- options: Map,
- ->
-
- Log.d(TAG, "startAndroidOngoingNotification called")
-
- val ongoingNotificationOptions =
- AndroidOngoingNotificationOptions(
- notificationId = options["notificationId"] as? String,
- channelId = options["channelId"] as? String,
- smallIcon = options["smallIcon"] as? String,
- deepLinkUrl = options["deepLinkUrl"] as? String,
- requestPromotedOngoing = options["requestPromotedOngoing"] as? Boolean,
- fallbackBehavior = options["fallbackBehavior"] as? String,
- )
-
- val result =
- runBlocking {
- notificationManager.startOngoingNotification(payload, ongoingNotificationOptions)
- }
-
- Log.d(TAG, "startAndroidOngoingNotification returning: $result")
- mapOf(
- "ok" to result.ok,
- "notificationId" to result.notificationId,
- "action" to result.action,
- "reason" to result.reason,
- )
- }
-
- AsyncFunction("updateAndroidOngoingNotification") {
- notificationId: String,
- payload: String,
- options: Map?,
- ->
-
- Log.d(TAG, "updateAndroidOngoingNotification called with notificationId=$notificationId")
-
- val ongoingNotificationOptions =
- AndroidOngoingNotificationOptions(
- channelId = options?.get("channelId") as? String,
- smallIcon = options?.get("smallIcon") as? String,
- deepLinkUrl = options?.get("deepLinkUrl") as? String,
- requestPromotedOngoing = options?.get("requestPromotedOngoing") as? Boolean,
- fallbackBehavior = options?.get("fallbackBehavior") as? String,
- )
-
- val result =
- runBlocking {
- notificationManager.updateOngoingNotification(
- notificationId,
- payload,
- ongoingNotificationOptions,
- )
- }
-
- Log.d(TAG, "updateAndroidOngoingNotification returning: $result")
- mapOf(
- "ok" to result.ok,
- "notificationId" to result.notificationId,
- "action" to result.action,
- "reason" to result.reason,
- )
- }
-
- AsyncFunction("upsertAndroidOngoingNotification") {
- payload: String,
- options: Map,
- ->
-
- Log.d(TAG, "upsertAndroidOngoingNotification called")
-
- val ongoingNotificationOptions =
- AndroidOngoingNotificationOptions(
- notificationId = options["notificationId"] as? String,
- channelId = options["channelId"] as? String,
- smallIcon = options["smallIcon"] as? String,
- deepLinkUrl = options["deepLinkUrl"] as? String,
- requestPromotedOngoing = options["requestPromotedOngoing"] as? Boolean,
- fallbackBehavior = options["fallbackBehavior"] as? String,
- )
-
- val result =
- runBlocking {
- notificationManager.upsertOngoingNotification(payload, ongoingNotificationOptions)
- }
-
- Log.d(TAG, "upsertAndroidOngoingNotification returning: $result")
- mapOf(
- "ok" to result.ok,
- "notificationId" to result.notificationId,
- "action" to result.action,
- "reason" to result.reason,
- )
- }
-
- AsyncFunction("stopAndroidOngoingNotification") { notificationId: String ->
- Log.d(TAG, "stopAndroidOngoingNotification called with notificationId=$notificationId")
- val result = notificationManager.stopOngoingNotification(notificationId)
- mapOf(
- "ok" to result.ok,
- "notificationId" to result.notificationId,
- "action" to result.action,
- "reason" to result.reason,
- )
- }
-
- Function("isAndroidOngoingNotificationActive") { notificationId: String ->
- notificationManager.isOngoingNotificationActive(notificationId)
- }
-
- Function("getAndroidOngoingNotificationStatus") { notificationId: String ->
- val status = notificationManager.getOngoingNotificationStatus(notificationId)
- mapOf(
- "isActive" to status.isActive,
- "isDismissed" to status.isDismissed,
- "isPromoted" to status.isPromoted,
- "hasPromotableCharacteristics" to status.hasPromotableCharacteristics,
- )
- }
-
- AsyncFunction("endAllAndroidOngoingNotifications") {
- notificationManager.endAllOngoingNotifications()
- }
-
- Function("canPostPromotedAndroidNotifications") {
- notificationManager.canPostPromotedAndroidNotifications()
- }
-
- Function("getAndroidOngoingNotificationCapabilities") {
- val capabilities = notificationManager.getOngoingNotificationCapabilities()
- mapOf(
- "apiLevel" to capabilities.apiLevel,
- "notificationsEnabled" to capabilities.notificationsEnabled,
- "supportsPromotedNotifications" to capabilities.supportsPromotedNotifications,
- "canPostPromotedNotifications" to capabilities.canPostPromotedNotifications,
- "canRequestPromotedOngoing" to capabilities.canRequestPromotedOngoing,
- )
- }
-
- AsyncFunction("openAndroidNotificationSettings") {
- notificationManager.openPromotedNotificationSettings()
- }
-
- // Android Widget APIs
-
- AsyncFunction("updateAndroidWidget") {
- widgetId: String,
- jsonString: String,
- options: Map,
- ->
-
- Log.d(TAG, "updateAndroidWidget called with widgetId=$widgetId")
-
- val deepLinkUrl = options["deepLinkUrl"] as? String
-
- widgetManager.writeWidgetData(widgetId, jsonString, deepLinkUrl)
-
- runBlocking {
- widgetManager.updateWidget(widgetId)
- }
-
- Log.d(TAG, "updateAndroidWidget completed")
- }
-
- AsyncFunction("reloadAndroidWidgets") { widgetIds: ArrayList? ->
- Log.d(TAG, "reloadAndroidWidgets called with widgetIds=$widgetIds")
-
- runBlocking {
- widgetManager.reloadWidgets(widgetIds)
- }
-
- Log.d(TAG, "reloadAndroidWidgets completed")
- }
-
- AsyncFunction("clearAndroidWidget") { widgetId: String ->
- Log.d(TAG, "clearAndroidWidget called with widgetId=$widgetId")
-
- widgetManager.clearWidgetData(widgetId)
-
- runBlocking {
- widgetManager.updateWidget(widgetId)
- }
-
- Log.d(TAG, "clearAndroidWidget completed")
- }
-
- AsyncFunction("clearAllAndroidWidgets") {
- Log.d(TAG, "clearAllAndroidWidgets called")
-
- widgetManager.clearAllWidgetData()
-
- runBlocking {
- widgetManager.reloadAllWidgets()
- }
-
- Log.d(TAG, "clearAllAndroidWidgets completed")
- }
-
- AsyncFunction("getActiveWidgets") {
- val context = appContext.reactContext ?: return@AsyncFunction emptyList