WebView integration
Running the v1 module inside a React Native or Ionic shell: the page, the bridge, and the permissions.
New integrations should use Saphere Scan v2. What follows documents the v1 module inside a native shell.
The v1 module is a web application. In a mobile app it runs inside a WebView, on a small HTML page you ship with the bundle, and talks to the native side through whatever bridge the framework provides.
Three things decide whether it works at all:
- The camera permission must be granted natively, before the WebView loads. The web layer cannot request it for you.
- The WebView must allow inline media playback without a user gesture. Otherwise the camera stream never starts.
- The page must be able to load the module from the CDN, which means network access and, on Android, the right file-access flags.
React Native
The page you ship
Place an integration.html in the platform asset directories — android/app/src/main/assets/ and the iOS bundle.
Both shells on this page import from Production. A Test build of your app points at the other CDN, and at a token endpoint calling the matching API:
| Environment | CDN base |
|---|---|
| Production | https://cdn.saphere.ai |
| Test | https://cdn.test.saphere.ai |
See Environments — the two are separate accounts, so the keys do not carry over.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0">
<style>
html, body { padding: 0; margin: 0; overflow: hidden; }
#container { height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="module">
import { Handler } from "https://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.js"
const { ReactNativeWebView } = window
const options = {
createMeasure: {
// Points at YOUR server, which holds the API key.
strategy: "delegate",
url: "https://your-backend.example/api/saphere-measure"
},
// Everything the widget reports is forwarded to the native side.
onEvent: event => ReactNativeWebView.postMessage(JSON.stringify(event))
}
Handler.load("#container", options)
</script>
</body>
</html>
Do not embed the API key here
This file ships inside your app bundle. Anything in it can be read by unpacking the APK or the IPA — including an Authorization header written into createMeasure.headers.
Point url at your own backend and let it hold the key.
The native screen
import { WebView } from "react-native-webview"
import { Platform } from "react-native"
const uri = Platform.OS === "android"
? "file:///android_asset/integration.html"
: "./assets/integration.html"
export const ScanScreen = () => (
<WebView
source={{ uri }}
// Without these two the camera stream never starts on iOS.
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
javaScriptEnabled
// Needed to load the page from the asset directory on Android.
allowFileAccess
allowUniversalAccessFromFileURLs
originWhitelist={["*"]}
textZoom={100}
startInLoadingState
onMessage={({ nativeEvent: { data } }) => {
const event = JSON.parse(data)
if (event.type === "result")
console.log("variables:", event.variables)
}}
/>
)
| Prop | Why it matters |
|---|---|
allowsInlineMediaPlayback | iOS plays video full-screen by default, which breaks the layout |
mediaPlaybackRequiresUserAction={false} | Without it the stream waits for a tap that never comes |
allowFileAccess, allowUniversalAccessFromFileURLs | Android needs both to load from file:///android_asset and reach the network |
textZoom={100} | Stops the system font-size setting from breaking the layout |
onMessage | Receives what postMessage sends |
The bridge is one-way
ReactNativeWebView.postMessage carries events out of the WebView. There is no v1 mechanism for the native side to drive the widget — no way to cancel a measurement or change options from React Native once it is mounted.
If you need that, unmount the WebView.
Camera permission
Request it before rendering the WebView. A getUserMedia call inside a WebView whose host app lacks the OS permission fails immediately, and the user sees the widget’s “camera refused” screen with no system prompt to explain it.
import { PERMISSIONS, RESULTS, check, request } from "react-native-permissions"
const CAMERA = Platform.OS === "android"
? PERMISSIONS.ANDROID.CAMERA
: PERMISSIONS.IOS.CAMERA
export async function ensureCamera(): Promise<boolean> {
const status = await check(CAMERA)
if (status === RESULTS.GRANTED)
return true
return await request(CAMERA) === RESULTS.GRANTED
}
Manifests
iOS — Info.plist:
<key>NSCameraUsageDescription</key>
<string>Used to measure your vital signs from video.</string>
Write a real sentence. App Store review rejects a placeholder, and the user reads it at the moment they decide.
Android — AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
Ionic / Capacitor
The Capacitor shell is itself a WebView, so the widget mounts directly into the app — no inner page.
Load the classic build from the app template:
<!-- index.html -->
<script src="https://cdn.saphere.ai/saphere-scan/v1/js/main.min.js"></script>
Then mount it from a component:
import { Component } from "react"
declare global {
interface Window { Handler?: any }
}
export class ScanView extends Component {
private readonly options = {
createMeasure: {
strategy: "delegate",
url: "https://your-backend.example/api/saphere-measure"
},
onEvent: (event: { type: string }) => console.log(event.type, event)
}
componentDidMount() {
// The script tag has run by now; no bridge is needed, the widget is
// already inside the app's own WebView.
window.Handler?.load("#container", this.options)
}
async componentWillUnmount() {
await window.Handler?.destroy()
}
render() {
return <div id="container" style={{ height: "100vh" }} />
}
}
AndroidManifest.xml needs the same two permissions as above, plus the activity configuration that keeps the WebView from being recreated on rotation:
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" />
A recreated activity restarts the measurement
Without thoseconfigChanges, rotating the device destroys and recreates the activity — and with it the WebView. A measurement in progress is lost, and the user starts over.Checklist
- Camera permission requested natively, before the WebView appears
-
NSCameraUsageDescriptionwritten as a real sentence -
CAMERAandINTERNETdeclared on Android - Inline media playback allowed, user gesture not required
- No API key anywhere in the shipped page
-
configChangesset so rotation does not recreate the WebView - The page served or loaded from a secure origin