# Integrating liveCheck into your app

This guide is for a third-party developer embedding the liveCheck identity verification widget into their own website or mobile app. It assumes someone is already running the liveCheck backend somewhere (e.g. `https://verify.example.com`) — swap that URL for your operator's actual deployment.

## 1. Get your origin allowlisted

The widget only renders for origins the operator has explicitly allowed (`ALLOWED_ORIGINS` on their server). Before integrating, ask the operator to add:

- Your website's origin, e.g. `https://your-site.com`
- Your mobile app's chosen identifier origin, e.g. `https://your-app.your-company.com` (any scheme+host string you both agree on — the mobile OS doesn't enforce it, but the backend does, so pick something unambiguous)

Without this, `GET /widget` returns `403 Forbidden`.

## 2. Website integration (iframe)

```html
<iframe
  id="livecheck-frame"
  src="https://verify.example.com/widget?origin=https://your-site.com"
  allow="camera"
  width="420"
  height="640"
  style="border:0; border-radius:12px;">
</iframe>

<script>
  window.addEventListener("message", (event) => {
    // Always check the sender's origin before trusting the payload.
    if (event.origin !== "https://verify.example.com") return;
    if (event.data?.type !== "liveCheck:result") return;

    const { passed, confidence, liveness, face_match } = event.data;

    if (passed) {
      console.log("Verified", confidence);
      // e.g. enable the "Continue" button, or notify your backend
    } else {
      console.log("Not verified:", liveness?.reason || face_match?.reason);
    }
  });
</script>
```

Notes:
- `allow="camera"` is required or the browser will block webcam access inside the iframe.
- The `origin` query param must exactly match what the operator allowlisted, and it doubles as the `postMessage` target the widget sends results to — get it wrong and you'll get a 403 or silently miss the result message.
- Serve your page over HTTPS (or `localhost` for dev). Browsers block camera access in insecure contexts.

## 3. Mobile app integration (WebView)

Load the same URL in your WebView. The widget detects the runtime and sends the result via both `window.parent.postMessage` and `window.ReactNativeWebView.postMessage`, so no widget-side changes are needed for React Native. Native iOS/Android need one small bridge addition (below).

### React Native (`react-native-webview`)

```jsx
import { WebView } from "react-native-webview";

const VERIFY_URL = "https://verify.example.com/widget?origin=https://your-app.your-company.com";

function VerificationScreen() {
  return (
    <WebView
      source={{ uri: VERIFY_URL }}
      mediaCapturePermissionGrantType="grant" // Android: auto-grant camera to the WebView
      onMessage={(event) => {
        const result = JSON.parse(event.nativeEvent.data);
        if (result.type !== "liveCheck:result") return;
        if (result.passed) {
          // proceed
        } else {
          // show failure reason: result.liveness?.reason or result.face_match?.reason
        }
      }}
    />
  );
}
```

iOS also requires `NSCameraUsageDescription` in `Info.plist`; Android requires the `CAMERA` runtime permission to be granted before the WebView loads (react-native-webview does not request it for you on all versions — check your installed version's docs).

### Native iOS (`WKWebView`)

Add a script message handler so the page's `window.webkit.messageHandlers.liveCheck.postMessage(...)` call reaches native code — that hook isn't in the shipped widget by default, so add it in your app's own JS injection, or ask the operator to extend `static/widget.js`'s `postResult()` (it already isolates the "who do I notify" logic in one place). Simpler: react to the same `postMessage` the widget already emits by injecting a small listener via `evaluateJavaScript`, or just consume `window.ReactNativeWebView`-style messages by defining that same global in your injected JS before the page loads:

```swift
let contentController = WKUserContentController()
contentController.add(self, name: "liveCheck")
let bridgeScript = WKUserScript(
    source: """
    window.ReactNativeWebView = { postMessage: (msg) => window.webkit.messageHandlers.liveCheck.postMessage(msg) };
    """,
    injectionTime: .atDocumentStart,
    forMainFrameOnly: true
)
contentController.addUserScript(bridgeScript)

let config = WKWebViewConfiguration()
config.userContentController = contentController
config.allowsInlineMediaPlayback = true
// let webView = WKWebView(frame: ..., configuration: config)
```

```swift
func userContentController(_ userContentController: WKUserContentController,
                            didReceive message: WKScriptMessage) {
    guard message.name == "liveCheck",
          let data = (message.body as? String)?.data(using: .utf8),
          let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
    else { return }
    let passed = result["passed"] as? Bool ?? false
    // handle result
}
```

Add `NSCameraUsageDescription` to `Info.plist` and grant camera permission before presenting the WebView.

### Native Android (`WebView`)

```kotlin
class LiveCheckBridge(private val onResult: (String) -> Unit) {
    @JavascriptInterface
    fun postMessage(message: String) {
        onResult(message)
    }
}

webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(LiveCheckBridge { json ->
    // parse json, handle result — same shape as the postMessage payload
}, "ReactNativeWebView") // matches the global name widget.js already checks for

webView.webChromeClient = object : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest) {
        request.grant(request.resources) // after you've confirmed CAMERA permission yourself
    }
}

webView.loadUrl("https://verify.example.com/widget?origin=https://your-app.your-company.com")
```

Declare `<uses-permission android:name="android.permission.CAMERA" />` and request it at runtime before loading the page.

## 4. Result payload reference

Both delivery paths (`postMessage` and `ReactNativeWebView.postMessage`) send the same JSON shape:

```json
{
  "type": "liveCheck:result",
  "passed": true,
  "confidence": 0.87,
  "liveness": { "passed": true, "score": 0.91, "reason": "Blink detected" },
  "face_match": { "matched": true, "confidence": 0.83, "reason": "Face matches ID document" }
}
```

If liveness fails, `face_match` is `null` (the face-match step never runs).

## 5. Important: don't trust the client message for high-stakes decisions

`passed`/`confidence` are computed by the operator's backend, but delivered to *your* frontend via `postMessage`/WebView bridge — a channel your own client-side code controls. There is currently no signed, backend-verifiable receipt (e.g. a token your server could call the operator's API with to independently confirm the result). That means:

- Fine to use directly for UX gating (unlock a "Continue" button, show a success screen).
- **Not** safe, on its own, to gate something your backend cares about (releasing funds, approving an account) purely because your frontend received a `passed: true` message — that message could be replayed or fabricated by anyone with access to the page's JS runtime (e.g. via dev tools).
- If you need that guarantee, ask the operator whether they can add a signed verification receipt your backend can validate — this is not implemented in the current version.

## 6. Testing checklist

- [ ] Your origin is allowlisted (confirm `/widget?origin=...` returns `200`, not `403`)
- [ ] Camera permission prompts correctly on your target browsers/devices
- [ ] `postMessage`/WebView listener checks the sender origin (web) or is scoped to your own WebView instance (mobile) — don't blindly trust `event.data` from `*`
- [ ] Failure reasons (`liveness.reason`, `face_match.reason`) are surfaced to the user so they know what to fix (e.g. "blink again", "hold ID steadier")
