@sira-screen-share/support-react-native
v0.0.8
Published
Sira screen-share-with-annotation support SDK for React Native and Expo apps.
Downloads
935
Readme
@sira-screen-share/support-react-native
Sira-style screen-share-with-annotation support for React Native and Expo apps.
Same 6-digit code handoff as the web SDK, same agent dashboard, same annotation protocol — built for native apps.
- One npm package, one config plugin, one provider component.
- Android: full-screen MediaProjection capture — agent follows the customer across apps. One system consent dialog at session start.
- iOS: ReplayKit, app-only (no system dialog).
- Sessions only end on explicit End from the customer or the agent (no auto-end on backgrounding).
- iOS 14+, Android 8+ (API 26+).
Install
npm install @sira-screen-share/support-react-native react-native-webrtcAdd the config plugin to app.json:
{
"expo": {
"plugins": [
"@sira-screen-share/support-react-native"
]
}
}Mount the provider at the app root. publicKey is required — the
SDK has no production default because mobile apps send their bundle ID
as the origin, and a production key has to be allowlisted server-side
against your specific bundle ID:
import { SiraSupport } from "@sira-screen-share/support-react-native";
export default function App() {
return (
<SiraSupport publicKey="pk_live_acme">
<RootNavigator />
</SiraSupport>
);
}Three keys you can use:
| Key | Use for |
| --------------------- | ------------------------------------------------------------------------------ |
| pk_test | localhost-port allowlisted; for the harness, sim/emulator dev, unit tests |
| pk_demo | localhost + the public Sira demo origin; for staging / preview builds |
| pk_live_<slug> | provisioned per integrator, allowlisted to your iOS+Android bundle IDs |
To get a pk_live_<slug> for production, contact Sira with your bundle ID
(e.g., com.acme.payroll on both platforms) and we'll provision it.
Full prop surface:
<SiraSupport
publicKey="pk_live_acme" // required
appName="MyApp" // optional; for the Android priming dialog
>
<RootNavigator />
</SiraSupport>Trigger code entry from anywhere:
import { useSiraSupport } from "@sira-screen-share/support-react-native";
function HelpButton() {
const { openCodeEntry } = useSiraSupport();
return <Button title="Enter support code" onPress={openCodeEntry} />;
}Drop-in integration prompt
Paste this into Cursor / Claude Code / Copilot Chat / Cline / Windsurf or any coding agent. It has everything the agent needs to wire the SDK into an existing React Native or Expo codebase cleanly.
I want to integrate the npm package `@sira-screen-share/support-react-native`
into this codebase. It's a drop-in SDK that lets our support team view and
annotate a customer's screen via a 6-digit code the customer enters from a
Help menu.
Reference docs + source (fetch these if anything below is ambiguous):
- npm: https://www.npmjs.com/package/@sira-screen-share/support-react-native
- GitHub: https://github.com/Yebe-Abe/sira-app-sdk
Please do the full integration. Follow these rules carefully:
1. INSTALL the package and its WebRTC peer dep with the right package
manager for this repo (npm / pnpm / yarn / bun — infer from lockfiles):
@sira-screen-share/support-react-native
react-native-webrtc
2. ADD the Expo config plugin to app.json (or app.config.js / .ts). If the
project is bare React Native (no Expo config plugin support), skip this
step — the SDK still works, but the integrator must edit
AndroidManifest.xml and Info.plist by hand. Surface that to me if it
applies.
{
"expo": {
"plugins": [
"@sira-screen-share/support-react-native"
]
}
}
3. MOUNT the provider once at the very top of the app tree, wrapping the
rest of the app. Most RN apps have a single root component (App.tsx)
that returns a navigator or layout — wrap that. publicKey is required.
import { SiraSupport } from "@sira-screen-share/support-react-native";
// dev / sim / emulator (localhost-allowlisted)
<SiraSupport publicKey="pk_test">
<RootNavigator />
</SiraSupport>
// production: ask Sira for a pk_live_<slug> tied to YOUR bundle ID
<SiraSupport publicKey="pk_live_<slug>">
<RootNavigator />
</SiraSupport>
Public keys at a glance (pass via the `publicKey` prop):
- `pk_test` → allowlists localhost ports. Use for sim/emulator dev.
- `pk_demo` → allowlists localhost + the public Sira demo origin.
- `pk_live_<slug>` → provisioned per integrator, allowlisted to a specific
iOS+Android bundle ID. Required for shipping to the
App Store / Play Store. Ask Sira for one.
IF YOU DON'T KNOW the right key for this project, default to "pk_test"
and surface a TODO in your response asking the human to swap it for a
pk_live_<slug> before publishing.
4. FIND the existing Help affordance in this codebase — it's usually one
of: a "Help" / "Support" item in a settings screen, a "?" icon in a
header, a menu item inside a drawer or profile sheet, a chat-with-us
row in account settings. Search for "help", "support", "contact",
"faq", "assistance".
5. ADD a new item/button called "Enter support code" next to whatever you
found. The click handler opens the SDK's modal via the
`useSiraSupport()` hook:
import { useSiraSupport } from "@sira-screen-share/support-react-native";
function SupportCodeRow() {
const { openCodeEntry } = useSiraSupport();
return (
<Pressable onPress={openCodeEntry}>
<Text>Enter support code</Text>
</Pressable>
);
}
Match the existing component / styling patterns exactly — if the
project uses NativeBase / Tamagui / styled-components / a custom design
system, use that. If it uses StyleSheet.create, use that. DO NOT
introduce a new styling approach.
6. DON'T DO THESE THINGS:
• Don't manually request screen-recording permissions — the SDK handles
ReplayKit (iOS) and MediaProjection (Android) itself
• Don't add any styling to the SDK's modal or banner from outside —
they're isolated overlays, customize via the `banner` prop only
• Don't add env vars, API routes, or backend wiring — the SDK talks to
Sira's hosted server
• Don't pass a custom `serverUrl` unless I tell you to
• Don't refactor the existing Help UI — only add one new entry point
7. After the edits, tell me:
• Which file you added <SiraSupport> to
• Which file you added the trigger to and how it looks in context
• Whether you used the default production key or passed publicKey="pk_test"
• Whether the project required any AndroidManifest.xml / Info.plist edits
(only matters for bare RN, not Expo)
That's it.Capture
- Android: MediaProjection. The customer accepts a system consent dialog at session start; a mediaProjection-typed foreground service runs for the duration of the session and posts a notification. Captures the entire device — the agent follows the customer across apps.
- iOS: ReplayKit. No system dialog (only the OS-level red recording bar). Captures the host app's surface only — system-wide capture would require a Broadcast Extension, which the SDK doesn't ship.
Android system dialog
The SDK shows a brief priming screen explaining what's about to happen, then Android's MediaProjection picker appears. The customer must:
- Choose Entire screen (not "A single app")
- Tap Start now
Once they agree, the foreground service starts and the agent sees the customer's screen — even when the customer leaves your app. A foreground service notification appears in the system tray for the duration of the session.
iOS
iOS uses ReplayKit and shows no system dialog — only the OS-level red recording bar at the top of the screen. iOS sessions capture only the host app's surface (no system-wide capture without a Broadcast Extension, which the SDK doesn't ship). When the customer backgrounds your app on iOS, the agent's view freezes at the last frame; capture resumes when the customer returns to your app.
What happens when the customer leaves your app
| Platform | While customer is in another app | When they return to your app | | ---------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- | | Android | Agent sees the other app live (MediaProjection captures the whole device). | Agent's prior annotations are still on the host app. | | iOS | Agent's view freezes (ReplayKit suspends sample buffer delivery). | Capture resumes; annotations persist. |
Sessions do not auto-end when the customer backgrounds your app. They end only when:
- The customer taps End in the consent banner
- The agent taps End on the dashboard
- The WebRTC connection has been confirmed dead for 30+ seconds (network-failure grace)
Public API
| Surface | What it does |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| <SiraSupport> | Root provider. Owns the session state machine + in-session banner. |
| useSiraSupport() | { openCodeEntry, end }. Safe to call before mount (no-ops). |
| <SiraSupportTrigger> | Optional unstyled button that wraps openCodeEntry(). |
Provider props
<SiraSupport
publicKey="pk_live_..." // required
serverUrl="https://api.sira-screen-share.com" // optional — for self-hosted
android={{
priming: true, // default true; show the brief explainer screen before the MediaProjection picker
}}
banner={{ // defaults are loud and recommended
background: "#b00020", foreground: "#fff",
copy: "...", endLabel: "End",
}}
appName="MyApp"
onSessionStart={(sid) => analytics.track("sira_start", { sid })}
onSessionEnd={(reason, sid) => analytics.track("sira_end", { reason, sid })}
/>Bandwidth
- 8 fps steady state, bursting to 15 fps on detected motion.
- WebP-encoded, ~30–60 KB per frame at 1280px on the longest edge.
- Target steady-state: 200–400 kbps. Comfortable on mobile networks.
Versioning
This package follows the same beta cadence as the web SDK. Breaking changes possible in 0.0.x; first stable surface is 0.1.0.
Branding
The customer-facing modal (code entry) and Android priming screen ship pre-styled with Sira's brand tokens — the same orange (#f97316) primary and stone-* greyscale used in the agent dashboard and the rest of Sira's surface. Currently this isn't theme-prop-overridable; the package is @sira-screen-share and you're opting into Sira's visual identity for these screens. The in-session <ConsentBanner> is themable via the banner prop because that surface is recording-status UI where loud-red defaults are deliberate. A full theme prop covering all SDK UI is on the 0.1.0 roadmap.
0.0.8 — iOS annotation overlay: attach to host window so ReplayKit captures it
After 0.0.7 made drawings appear at correct positions on the customer's iPhone, live testing showed they still didn't appear on the dashboard — the customer saw the agent's strokes, but the captured JPEG frames coming back to the dashboard didn't include them.
Root cause: 0.0.1–0.0.7 attached the iOS annotation overlay as its own separate UIWindow at windowLevel = .alert + 1. ReplayKit's in-app startCapture doesn't include alert-level sibling windows in the captured composition — they render to screen for the customer but never make it into the captured frame buffer. (Android wasn't affected because its overlay is added to android.R.id.content, i.e. inside the captured view tree.)
The "captured frame is the single source of truth on the dashboard" design (introduced in the partial-render refactor) relies on overlay strokes being visible in the captured frames. Without that, the agent has no way to see what they drew once the local in-progress drag is released.
Fix: the overlay is now added as a subview of the host app's key UIWindow (using addSubview + bringSubviewToFront + autoresizing for orientation). It's still touch-transparent (isUserInteractionEnabled = false) so the host app's touches go through. Since it's in the same render tree the host app's main UIWindow uses, ReplayKit captures it.
Drops the no-longer-needed SiraPassthroughVC helper and the overlayWindow property.
No protocol or API change. Android unchanged.
0.0.7 — iOS annotation overlay: scale viewport-pixel coords to UIKit points
Now that frames flow on iOS (thanks to 0.0.6's JPEG switch), live testing surfaced that agent drawings appeared ~DPR-times larger and positioned wrong on the customer's iPhone — and consequently never made it back to the dashboard's view because they painted off-screen and the captured frame showed nothing where the agent expected.
Root cause: the SDK reports viewport as Dimensions.get("screen") × dpr (pixel space) and the dashboard sends annotation coords in that same pixel space, but the iOS overlay's UIView (bounds.size) is in UIKit points (1/DPR of pixels). Every iPhone since 2010 is Retina (DPR 2 or 3), so the overlay was effectively scaling coords up by 2-3× and ignoring the device pixel ratio. Android wasn't affected because its Canvas uses pixels natively.
Fix: translatePoint / translateRect in the iOS overlay now scale incoming coords by bounds / viewport before subtracting the screen-origin. One scale factor for x, one for y; works on iPhone full-screen, iPad split-view, multi-window — all the cases the existing screenOrigin() logic already handled.
No protocol change. Android consumers see zero behavioral difference.
0.0.6 — iOS frame encoder switched to JPEG; threading fix on stopCapture
Live test on a physical iPhone (the first time anyone ran iOS SDK code on real hardware) surfaced that iOS has no WebP encoder in ImageIO — Apple ships only decoders. Both 0.0.3's org.webmproject.webp UTI and 0.0.5's public.webp UTI fail at CGImageDestinationCreateWithData, silently dropping every iOS frame. 0.0.6 switches iOS to JPEG (public.jpeg, quality 0.6), the only lossy image format iOS ImageIO can both encode and that browsers decode universally. The dashboard's NativeFrameViewer was updated in lockstep to a format-agnostic Blob-based loader (browser sniffs from magic bytes), so the same code path renders WebP (Android) and JPEG (iOS) without a protocol change.
Also fixes a Main Thread Checker violation in stopCapture: ReplayKit's completion callback runs on its internal XPC queue, not main; the removeOverlay() call inside that callback was touching UIWindow.isHidden from a background thread. Wrapped in DispatchQueue.main.async.
JPEG frames are roughly 1.5–2× WebP at the same visual quality, so iOS sessions use ~500-800 kbps vs. Android's ~250-450 kbps at 8 fps. Within the 200-400 kbps target band; can drop quality to 0.5 in a follow-up if real-world bitrate runs hot. Long-term, linking libwebp.framework would let iOS match Android's output size.
The wire-format field is still named webp for backward compatibility — it's now opaque image bytes whose format is sniffed on decode.
0.0.5 — Sira-brand the modal + priming screen
Replaces the legacy React-Native blue (#1a73e8) CTA + arbitrary greys (#444, #333, #666) in CodeEntryModal and PrimingScreen with Sira's primary orange + stone palette. No API changes — purely visual. Drop-in compatible with 0.0.4.
0.0.4 — iOS bug fixes
iOS-only patch. Recommended for any iOS integration; Android consumers see no behavioral change.
- WebP encoding is fixed.
0.0.3(and earlier) used the wrong UTI string ("org.webmproject.webp") forCGImageDestinationCreateWithData, which made the destination init return nil and silently dropped every frame. Switched to"public.webp"— the identifier ImageIO actually registers on iOS 14+. Any iOS session before 0.0.4 produced no frames on the agent dashboard. requestProjectionConsentObj-C signature now matches the JS bridge (nocaptureModeparameter — that argument was dropped in 0.0.3 on the JS side but not here, so the call mismatched at runtime).RPScreenRecorderDelegateis now wired. When iOS terminates capture mid-session (Control Center toggle, low memory, Siri, screen-recording restriction kicking in), the SDK emits aSiraCaptureStateevent so the JS provider can tear the session down cleanly instead of waiting for the WebRTC peer to time out.- Frame pipeline thread safety:
seq/lastFrameTime/lastFrameHashare now mutated only on a dedicated serial queue;CIContextis cached on the module instead of recreated per frame. SiraFrameevent body now includests(epoch ms). Matches Android.
Migrating from 0.0.2
0.0.3 removed the "in-app" Android capture mode — full-screen MediaProjection is the only Android path now. If your earlier integration looked like:
// app.json
["@sira-screen-share/support-react-native", { "android": { "captureMode": "full-screen" } }]<SiraSupport android={{ captureMode: "full-screen" }} ... />…drop both { "android": { "captureMode": ... } } and the android={{ captureMode: ... }} prop. The plugin entry is now just "@sira-screen-share/support-react-native" (no options) and the provider's android prop carries only priming. The CaptureMode type export is gone.
The plugin always injects FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PROJECTION permissions and the SiraProjectionService declaration into the host manifest; that used to be conditional on captureMode === "full-screen".
License
MIT.
