@lumen-stack/expo
v0.2.0
Published
Expo / React Native helper for Lumen — answers screenshot requests with a real native screen capture.
Downloads
371
Readme
@lumen-stack/expo
Native screenshot bridge for running Lumen inside an Expo / React Native app.
The Lumen web SDK captures screenshots with html2canvas, a DOM re-renderer — it
can't match native rendering and drops native overlays plus the live text typed into
native <TextInput>s. This package lets the native side take a real React Native
snapshot and hand the pixels back to Lumen over the WebView bridge.
Install
npx expo install @lumen-stack/expo react-native-webview react-native-view-shotreact, react-native, react-native-webview, and react-native-view-shot are peer
dependencies.
Use the drop-in LumenWebView
Render LumenWebView where you'd render a <WebView>. It intercepts Lumen capture
requests and answers them with a native screenshot of the WebView by default; your own
onMessage still fires for everything else.
import { LumenWebView } from "@lumen-stack/expo";
export function Feedback() {
return <LumenWebView source={{ uri: "https://your-app.example" }} />;
}Inside the web app that loads in the WebView, force the native path so it never silently falls back to the (DOM) html2canvas renderer:
import { LumenProvider, createNativeCaptureProvider } from "@lumen-stack/react";
<LumenProvider
apiKey="lk_pub_…"
capture={{ mode: "custom", provider: createNativeCaptureProvider() }}
>
{/* …your app… */}
</LumenProvider>;iOS native navigation/tab bars
On iOS, full key-window capture can force UIKit to commit pending screen updates. With
native chrome such as UITabBarController, UINavigationBar, or Liquid Glass
UIVisualEffectView, that can blank the real native bar until the next UIKit relayout
and can interrupt in-flight sheet animations. This is a react-native-view-shot
captureScreen() caveat: it snapshots the whole key window with
afterScreenUpdates: true.
LumenWebView uses its own WebView ref as the default capture target, which keeps native
tab/navigation chrome out of the render pass. If you need to include more React Native UI
around the WebView, pass a React Native root or container ref as the capture target.
Lumen will use captureRef(target, …) instead of key-window captureScreen(), so UIKit
does not re-render native chrome outside that subtree. The screenshot will include the
targeted subtree, and will exclude native chrome outside it.
import { useRef } from "react";
import { View } from "react-native";
import { LumenWebView } from "@lumen-stack/expo";
export function FeedbackScreen() {
const captureRootRef = useRef<View>(null);
return (
<View ref={captureRootRef} collapsable={false} style={{ flex: 1 }}>
<LumenWebView
captureTarget={captureRootRef}
capture={{ settleDelayMs: 120 }}
source={{ uri: "https://your-app.example" }}
/>
</View>
);
}collapsable={false} is important because React Native may otherwise remove a purely
layout-only wrapper from the native view hierarchy, leaving captureRef without a real
native view to snapshot.
Bring your own WebView
If you manage the WebView yourself, call handleLumenMessage from onMessage:
import { useRef } from "react";
import { WebView } from "react-native-webview";
import { handleLumenMessage } from "@lumen-stack/expo";
const ref = useRef<WebView>(null);
<WebView
ref={ref}
source={{ uri: "https://your-app.example" }}
onMessage={(e) => {
if (handleLumenMessage(ref.current, e.nativeEvent.data)) return;
// …your own message handling…
}}
/>;To use a targeted capture with your own WebView, pass the same options to
handleLumenMessage. For Lumen messages it returns the capture promise; for non-Lumen
messages it returns false.
const handled = handleLumenMessage(ref.current, e.nativeEvent.data, {
captureTarget: captureRootRef,
settleDelayMs: 120,
});
if (handled) {
void handled.finally(() => {
// Optional: trigger any host relayout/repair work after native capture settles.
});
return;
}Capture options
| Option | Purpose |
| ---------------------- | ----------------------------------------------------------------------- |
| format | Screenshot format, "png" or "jpg". Default "png". |
| quality | JPG quality from 0 to 1. Default 1. |
| captureTarget | React Native ref, React instance, or react tag captured via captureRef. |
| settleDelayMs | Extra delay after two animation frames before capture; clamped 0–500. |
| afterScreenUpdates | iOS hint threaded to capture. false avoids forcing a screen-update commit when a compatible native snapshotter is installed. |
The web SDK may also request afterScreenUpdates: false; @lumen-stack/expo now honors
that value. On iOS, the package ships a small autolinked LumenCapture native module
that uses drawViewHierarchyInRect:afterScreenUpdates:NO for key-window capture when
that flag is false. Native navigation apps should still use LumenWebView's default
targeted capture or pass a captureTarget root/container ref, because targeted capture
keeps native chrome outside the subtree out of the render pass entirely. Low-level
respondToCaptureRequest calls with no target fall back to react-native-view-shot
key-window captureScreen() when the native module is unavailable.
Shake to open
Wire any shake detector to postLumenShake; the web SDK opens the sheet when
shake-to-open is enabled:
import { Accelerometer } from "expo-sensors";
import { postLumenShake } from "@lumen-stack/expo";
Accelerometer.addListener(({ x, y, z }) => {
if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 2.5) postLumenShake(ref.current);
});Keyboard inset
Inside an iOS WKWebView window.visualViewport does not shrink when the soft
keyboard opens, so the web SDK can't keep the floating trigger and the feedback
input above the keyboard on its own. LumenWebView forwards the native keyboard
height automatically (forwardKeyboardInset, default true); Lumen then lifts
its trigger and modal input above it.
Bringing your own WebView? Push the height yourself:
import { Keyboard } from "react-native";
import { postLumenKeyboardInset } from "@lumen-stack/expo";
Keyboard.addListener("keyboardDidShow", (e) =>
postLumenKeyboardInset(ref.current, e.endCoordinates.height),
);
Keyboard.addListener("keyboardDidHide", () =>
postLumenKeyboardInset(ref.current, 0),
);API
| Export | Purpose |
| ----------------------------- | ----------------------------------------------------------------- |
| LumenWebView | Drop-in WebView that auto-answers capture requests + forwards the keyboard height. |
| handleLumenMessage(w, d) | Handle a WebView message; returns the capture promise or false. |
| respondToCaptureRequest | Low-level: capture + reply to a specific request id. |
| postLumenShake(w) | Open the Lumen sheet by simulating a shake in the WebView. |
| postLumenKeyboardInset(w, px) | Forward the soft-keyboard height so Lumen lifts above it. |
| parseCaptureRequest(d) | Pure parser for capture-request messages. |
