feat: dynamic widgets for iOS and Android#201
Draft
V3RON wants to merge 3 commits into
Draft
Conversation
Closes #165 > [!WARNING] > **Experimental.** Client-rendered widgets are usable in production at your own > risk — the public API and generated build output may change between releases. ## Summary Adds **client-rendered widgets on iOS**: widgets written as plain `(props, env) => JSX` functions, evaluated on-device in the widget extension via JavaScriptCore on every render. The `env` argument carries live device state (`widgetFamily`, `colorScheme`, `widgetRenderingMode`, `locale`, `showsWidgetContainerBackground`) captured per render from SwiftUI's `@Environment`, so a widget reacts to the home-screen environment (family resize, dark-mode flip, tinted-stack mode) without a native rebuild — the same model as `expo-widgets`. Widgets can also be **user-configurable via a native AppIntent "Edit Widget" sheet** (iOS 17+): parameters declared in `app.json` (with code-defined defaults) are surfaced by WidgetKit's configuration UI and delivered to the widget as `env.configuration` on each render. Sits alongside the existing server-rendered path (opt-in, per-widget, no migration). The on-device UI goes through Voltra's existing JSON wire format + Swift renderer, so client-rendered widgets are visually identical to server-rendered ones. iOS only in this PR; the Android port (reusing Track 4's standalone-Hermes JNI) is a follow-up. ## The two goals (both met) 1. **Env parity with `expo-widgets`** — `WidgetEnvironment` flows into the on-device `render(props, env)` automatically, captured from `@Environment` in the widget extension. Includes user `configuration` from a native AppIntent **Edit Widget** sheet (see below). 2. **Dev hot reload** — editing a widget's JSX refreshes the pinned home-screen widget in dev, no rebuild. ## How it works **Opt-in.** A widget component carrying the `'use voltra'` directive is auto-detected at prebuild and registered as client-rendered. The `app.json` widget `id` must equal the JSX component name (the plugin fails loud on mismatch — the id is both the bundle URL suffix and the WidgetKit `kind`). **Configuration via AppIntent (iOS 17+).** Declaring `appIntent: { parameters: [{ name, title, default }] }` on a widget makes the plugin generate a `WidgetConfigurationIntent` + `AppIntentConfiguration`, so users edit parameters through the native **Edit Widget** sheet. Defaults come from code (`parameters[].default`); the configured values arrive as `env.configuration` on each render. **Dev.** Widgets are discovered by a filesystem scan + watcher (no manual side-effect imports). The widget's JS bundle is served by Metro; the extension fetches and evaluates it per render. `enableWidgetHotReload()` (called once at host-app startup, DEV-only) hooks Metro's `__accept` so a JSX edit triggers `WidgetCenter.reloadAllTimelines()` → the provider re-fetches the fresh bundle. A generated per-platform **dev barrel** keeps widget modules in the host app's Metro graph so Fast Refresh actually fires for widget-only edits and the widget Metro server stays fresh. **Production (release).** A release-only Xcode build phase runs the project's widget bundler (`example/metro/bundleWidgets.js`), baking each `voltra-widget-<id>.bundle` (plain JS for v1) into the widget extension; the native release loader reads it from `Bundle.main`. Debug uses Metro; the phase no-ops there. ## iOS runtime (`packages/ios-client/ios/`) - `VoltraJSRenderer.swift` — one shared `JSContext` per extension process; captures each bundle's exports under `globalThis.__voltraWidgets[<id>]`. `ensureEvaluated()` lets the View re-evaluate from the entry's carried bundle source, so rendering survives WidgetKit re-rendering an archived entry in a fresh process. - `VoltraClientWidgetRuntime.swift` — dual-path bundle loader (Metro in DEBUG, baked asset in release), env capture in the ContentView (`@Environment` reads are only valid in a View body, hence the Provider/View split), and the generated `AppIntentTimelineProvider` plumbing for configured widgets. ## Plugin (`packages/ios-client/expo-plugin/src/`) - `'use voltra'` detection (Babel scan), generated Swift (`AppIntentConfiguration` gated `iOS 17+`), prerendered initial state, and the release widget-bundling build phase. Emits a one-time **EXPERIMENTAL** warning at prebuild. ## Verification - **Dev (simulator):** env capture, AppIntent config (Edit Widget → `env.configuration`), and hot reload all verified end-to-end. - **Production bake:** verified at build/artifact level — `voltra-widget-<id>.bundle` is baked into the installed app, and a full clean Release build is green (including app JS bundling). ## Known limitations - **Release render needs real-device verification.** On the iOS **Simulator**, a release build does not invoke the `AppIntentConfiguration` timeline (WidgetKit stays on the redacted placeholder) — independent of the baked bundle, and consistent with the simulator's general unreliability for widget rendering. The bake itself is verified; the on-device render should be confirmed on a physical device. - **Metro scaffolding lives in the example app** (`example/metro/`) for now; consumers copy it. Productizing it into the package is a follow-up. - **`.hbc` precompilation** is deferred — v1 bakes plain JS. ## Changeset `@use-voltra/ios-client` minor — experimental client-rendered widgets. --------- Co-authored-by: Szymon Chmal <szymon@chmal.it>
Closes #165 > [!WARNING] > **Experimental.** Client-rendered widgets are usable in production at your own risk — the public API and generated build output may change between releases. ## Summary Adds **client-rendered widgets on Android**: widgets written as plain `(props, env) => JSX` functions, evaluated on-device in a **standalone Hermes runtime** (custom JNI/CMake, built on Track 4's engine) on every render. The `env` argument carries live device state (`widgetFamily`, `colorScheme`, `locale`, `materialColors`, `configuration`, `build`) captured per render from the Glance composition context, so a widget reacts to the home-screen environment (size, dark-mode flip, Material You colors) without a native rebuild — the same model as `expo-widgets`, and the Android counterpart of #190. Sits alongside the existing server-rendered path (opt-in, per-widget, no migration). The on-device UI goes through Voltra's existing JSON wire format + Glance renderer, so client-rendered widgets are visually identical to server-rendered ones. ## The two goals (both met) 1. **Env parity with `expo-widgets`** — `WidgetEnvironment` flows into the on-device `render(props, env)` automatically, captured in `provideGlance`. Includes Material You dynamic colors (`env.materialColors`) and user `configuration`. 2. **Dev hot reload** — editing a widget's JSX refreshes the pinned home-screen widget in dev, no rebuild. ## How it works **Opt-in.** A component carrying the `'use voltra'` directive is auto-detected at prebuild (via `@use-voltra/compiler`) and registered as client-rendered. The `app.json` widget `id` must equal the JSX component name (the plugin fails loud on mismatch — the id is both the bundle URL suffix and the widget identity). **Configuration.** Parameters declared in `app.json` (`appIntent.parameters`, with code-defined defaults) are baked to `assets/voltra/widget_config_defaults.json` and surface as `env.configuration`. Runtime values set via `setWidgetConfiguration` (stored in DataStore) override the defaults. Android has no system widget-configuration UI, so this in-app API is the stand-in for iOS's AppIntent "Edit Widget" sheet. **Dev.** Widgets are discovered by a filesystem scan + watcher; the JS bundle is served by Metro and fetched per render. `enableWidgetHotReload()` (called once at host-app startup, DEV-only) hooks Metro's `__accept` so a JSX edit triggers `reloadAndroidWidgets()` → `provideGlance` re-fetches the fresh bundle. A generated per-platform **dev barrel** keeps widget modules in the host app's Metro graph so Fast Refresh fires for widget-only edits. **Production (release).** A release-only Gradle task (`voltraBundleWidgets`) resolves `@use-voltra/metro/bundle-widgets` and bakes each `voltra-widget-<id>.bundle` (plain JS for v1) into `assets/voltra/`; the native release loader reads it from app assets. Debug uses Metro; the task is wired only into release asset-merge. ## Android runtime (`packages/android-client/android/`) - `cpp/voltra_js_renderer.cpp` + `VoltraJSRenderer.kt` — standalone Hermes via custom JNI (`hermes-android` prefab); `evaluateBundle` captures each widget's `render`, invoked per Glance render. Perf logs gated behind `NDEBUG`/`BuildConfig.DEBUG`. - `VoltraClientGlanceWidget.kt` — dual-path bundle loader (Metro in dev, baked asset in release), env capture in `provideGlance` (size/scheme/locale/Material You/configuration), DataStore-backed configuration. ## Shared (`packages/metro`, `packages/compiler`) - `@use-voltra/metro` generates a **per-platform render shim** so the one generated widget entry renders via `renderAndroidVariantToJson` on Android / `renderVoltraVariantToJson` on iOS. - `@use-voltra/compiler` `scanVoltraDirectives` is the shared `'use voltra'` detector used by the Android plugin. ## Plugin (`packages/android-client/expo-plugin/`) `'use voltra'` detection, generated Glance receiver, prerendered initial state, baked config defaults, and the release widget-bundling Gradle task. Emits a one-time **EXPERIMENTAL** warning at prebuild. ## Verification (clean emulator, end-to-end) - **Dev:** env capture, config code-default (`Hello`) + runtime override (`World`), and hot reload (edit → ~1s → pinned widget updates) all verified live. - **Production bake:** **release APK with Metro stopped** renders the full live env on-device from the baked asset bundle (verified visually — live timestamp confirms a real render, not the placeholder). Stronger than #190, which deferred release render to a real device. ## Known limitations - **`.hbc` precompilation** is deferred — v1 bakes plain JS. - Emulator widget hot reload is reliable for normal editor saves; tools that replace files via atomic rename (e.g. `sed -i`) can be missed by the file watcher. ## Stacking Targets `main` and stacks on #190 (iOS). Until #190 merges, this PR's diff includes the shared iOS work; it narrows to Android-only once #190 lands. Review/merge after #190. ## Changeset `@use-voltra/android-client` minor — experimental client-rendered widgets (render shim covered by the `@use-voltra/metro` bump). --------- Co-authored-by: Szymon Chmal <szymon@chmal.it>
Document client-rendered and configurable widgets for iOS and Android.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.