Skip to main content
@run-cloud/ui renders the signed viewer URL returned for either an iOS simulator or an Android emulator. It adds embed mode, validates browser messages against the iframe window and its exact origin, exposes typed lifecycle and interaction events, and removes its listeners and timers when it unmounts. The interaction payloads, coordinates, keys, buttons, and platform capability matrix are documented in Control a Simulator.

Install

The React package needs only a signed session URL. Create sessions with the server-side @run-cloud/sdk client or the run.cloud API. Never put a run.cloud API key in a browser bundle.

Create a session on the server

Install the server SDK separately:
First upload a platform-compatible app build as an asset. Keep the resulting asset id on the server. The same helper can then install and launch that app on either platform. session.url is the signed viewer URL passed to the React component:
Call createSimulatorSession from an authenticated server route, return only the session metadata and signed URL that the authorized browser needs, and call releaseSimulatorSession from a server route when the preview is finished. For scripts and test jobs, put cloud.simulators.delete(...) in finally. The signed URL is a bearer secret scoped to the session. Do not place it in logs, analytics, screenshots, or public page URLs.

Render the React component

Copy this component as-is into a React application. Its parent has an explicit, responsive size; RemoteControl fills that area and the embedded viewer adapts to the available space.
The module is safe to import during server rendering. In Next.js, put the interactive component behind a "use client" boundary because refs, state, and event callbacks run in the browser. Unmounting RemoteControl removes its iframe message listener and availability timer. It does not release the metered run.cloud session. Always call your server-side release path with the session id and platform.

Component props

RemoteControl accepts the iframe attributes id, title, className, style, allow, draggable, allowTransparency, onLoad, and onError. These props control the iframe without changing the signed session URL. The component adds embed=1. When loadingGuard is true, it also adds loadingGuard=1. It preserves the signed token and every other query parameter.

Events

RemoteControlStatus includes streaming, appLaunched, reactNative, device, bundleId, expectedBundleId, orientation, and accessibilityOverlay. Treat the component as ready when:

Controls

Use the acknowledged RemoteControlHandle methods for automation. They resolve with the matching typed result and reject with RemoteControlInteractionError when the iframe reports a failure: setOrientation accepts the shared four-value orientation type. Current iPhone Simulator sessions support portrait, landscape_left, and landscape_right; portrait_upside_down rejects with a correlated, non-retryable unsupported_action failure. Android Emulator sessions support all four values. Options accept requestId, timeoutMs, and signal. The timeout defaults to 15,000 milliseconds and accepts 100 to 60,000. Aborting, timing out, changing the frame, or removing the component sends a correlated cancellation request before rejecting the pending promise. Cancellation is best effort and cannot undo an action the simulator already completed. The older fire-and-forget command channel remains available through sendCommand, reload, home, rotate, screenshot, and toggleAccessibility. It accepts: On current iPhone Simulator sessions, relative viewer rotation skips portrait_upside_down so the legacy control stays within renderable states. Each accepted command produces onCommandResult with command, ok, and an optional error string. Prefer acknowledged interactions for tests and automation.

Session timeouts

New sessions default to a 60-second inactivity timeout when the option is omitted. Set a duration explicitly for predictable product behavior, or pass inactivityTimeout: null in the TypeScript SDK (--inactivity-timeout none in the CLI) to disable inactivity auto-close. Hard timeouts and explicit release still apply. Pointer, touch, wheel, and keyboard input inside the iframe reset an enabled inactivity timer. Watching the stream without interacting does not. The viewer shows the final countdown and emits a session-ended event when the lease closes.

Raw iframe alternative

You can use the signed URL without React. Add embed=1, listen only to the expected iframe window and exact URL origin, and use that same exact origin when sending commands:
For compatibility, both iOS simulator and Android emulator iframes use the ios-simulator:* lifecycle and legacy-command message names. Acknowledged interactions use platform-neutral run-cloud:* names. Removing a raw iframe or its message listener also does not release its cloud session. Call the authenticated server-side delete operation separately.