Skip to main content
@runcloud/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. Accessibility reads use a separate 20,000-millisecond default and accept requestId, timeoutMs, and signal. The response remains bound to the exact current iframe window and signed origin. See Read the Accessibility Tree for the schema, redaction behavior, and an embed example. 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.

Copy and send clipboard text

RemoteControl exposes getClipboard() and setClipboard(text) on its ref for both iOS and Android. To transfer text between the simulator and the user’s clipboard, render the supplied controls next to the viewer:
“Copy from Simulator Clipboard” writes simulator text to the user’s clipboard. “Send to Simulator Clipboard” reads the user’s clipboard and sends its text to that simulator. The controls report permission denial, unavailable clipboard access and transfer failures, and cancel pending transfers when sessionKey changes. Use a new key whenever the selected session or device changes. Browser clipboard access requires HTTPS (or localhost), a focused page and a user click. The browser may prompt for permission. If you override the iframe’s allow attribute, retain clipboard-read; clipboard-write. The containing page’s Permissions Policy must also permit the desired clipboard operations. No transfer happens automatically on page load. For custom controls, copyFromSimulatorClipboard(getText, signal?) and sendToSimulatorClipboard(setText, signal?) perform the browser part. Call them directly in a click handler and display any error. The read callback returns (await control.current.getClipboard()).result.text; the write callback calls control.current.setClipboard(text). Cancel both the callback’s interaction and the browser transfer when the selected session changes. Raw iframe requests use the existing acknowledged contract:
Verify the reply’s exact origin and event.source === iframe.contentWindow, then match its request ID and action. A successful run-cloud:interaction-result contains result.text for reads and result.characterCount for writes. These messages access the simulator clipboard; the parent page owns user clipboard permissions and success/error feedback. Never include an API key in a public page: create the session on your server with your run.cloud credentials and pass its signed viewer URL to the authorized user.