> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edpire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile & Cross-Platform Integration

> How to integrate Edpire into Flutter, React Native, native iOS/Android, or any mobile app.

Edpire's REST API and embed token system are designed to work with any platform, including mobile apps that cannot run the JavaScript SDK directly.

## How it works

Mobile integration follows a two-part architecture:

| Part                                                            | Who does it    | How                                   |
| --------------------------------------------------------------- | -------------- | ------------------------------------- |
| **API calls** (list assessments, submit answers, fetch results) | Your backend   | REST API with Bearer token            |
| **Assessment player**                                           | Mobile WebView | UMD SDK loaded in a minimal HTML page |

Your API key never leaves your server. The mobile app only ever sees a short-lived embed token.

```
Mobile app  →  YOUR backend  →  POST /api/v1/embed/token  →  Edpire
                                                                    ↓
Mobile app  ←  { token, expires_at }  ←  YOUR backend
     ↓
WebView loads player HTML with token
     ↓
onComplete message posted to native layer
```

***

## The WebView HTML template

This is the universal mobile player. Load it in any WebView with the embed token injected:

```html theme={null}
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: sans-serif; }
    #root { min-height: 100vh; }
  </style>
</head>
<body>
  <div id="root"></div>
  <script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.5/dist/umd/index.global.js"></script>
  <script>
    var TOKEN = "{{EMBED_TOKEN}}";  // inject server-side before loading in WebView

    EdpireSDK.EdpireAssessment.mount({
      token: TOKEN,
      container: "#root",
      onComplete: function(result) {
        // Post result back to native layer
        if (window.ReactNativeWebView) {
          window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: result }));
        }
        if (window.flutter_inappwebview) {
          window.flutter_inappwebview.callHandler("onComplete", result);
        }
      },
      onError: function(err) {
        if (window.ReactNativeWebView) {
          window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: err }));
        }
        if (window.flutter_inappwebview) {
          window.flutter_inappwebview.callHandler("onError", err);
        }
      }
    });
  </script>
</body>
</html>
```

Serve this HTML from your own backend (or build it as a string in your app) with `{{EMBED_TOKEN}}` replaced server-side.

<Note>
  Never embed your API key in the WebView HTML. The embed token is short-lived (2 hours), consumed on submit, and scoped to a single learner and assessment — it is safe to put in client code.
</Note>

***

## React Native

React Native apps can use `EdpireClient` directly for server-side operations — it uses `fetch`, which is available natively in React Native. For the player, open a `WebView` with the HTML template.

**Server-side (your Node.js backend):**

```typescript theme={null}
import { EdpireClient } from "@edpire/sdk/client"

const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })

// Your API route that the React Native app calls:
export async function POST(req: Request) {
  const { assessmentId, learnerRef } = await req.json()
  const token = await client.mintEmbedToken(assessmentId, learnerRef)
  return Response.json(token)
}
```

**React Native app:**

```tsx theme={null}
import { useState } from "react"
import { WebView } from "react-native-webview"

const PLAYER_HTML = `
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
  <div id="root"></div>
  <script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.5/dist/umd/index.global.js"></script>
  <script>
    EdpireSDK.EdpireAssessment.mount({
      token: "EMBED_TOKEN_PLACEHOLDER",
      container: "#root",
      onComplete: function(r) {
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }));
      },
      onError: function(e) {
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }));
      }
    });
  </script>
</body>
</html>
`

export function AssessmentScreen({ assessmentId, learnerRef }) {
  const [html, setHtml] = useState<string | null>(null)

  async function launch() {
    // Call YOUR backend — never call Edpire directly from the app
    const res = await fetch("https://yourapp.com/api/embed-token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ assessmentId, learnerRef }),
    })
    const { token } = await res.json()
    setHtml(PLAYER_HTML.replace("EMBED_TOKEN_PLACEHOLDER", token))
  }

  if (!html) return <Button onPress={launch} title="Start Assessment" />

  return (
    <WebView
      source={{ html }}
      onMessage={(event) => {
        const msg = JSON.parse(event.nativeEvent.data)
        if (msg.type === "complete") {
          console.log("Score:", msg.result.score, "/", msg.result.max_score)
        }
      }}
    />
  )
}
```

***

## Flutter

Flutter apps call your backend via `http` or `dio` to fetch the embed token, then load the player HTML in a WebView.

**Mint the token from your backend (Dart HTTP call):**

```dart theme={null}
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> mintEmbedToken(String assessmentId, String learnerRef) async {
  // Call YOUR backend — the API key lives there, not in the app
  final res = await http.post(
    Uri.parse('https://yourapp.com/api/embed-token'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'assessmentId': assessmentId, 'learnerRef': learnerRef}),
  );
  final data = jsonDecode(res.body) as Map<String, dynamic>;
  return data['token'] as String;
}
```

**Load the player in a WebView (using `flutter_inappwebview`):**

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class AssessmentScreen extends StatefulWidget {
  final String assessmentId;
  final String learnerRef;
  const AssessmentScreen({required this.assessmentId, required this.learnerRef, super.key});

  @override
  State<AssessmentScreen> createState() => _AssessmentScreenState();
}

class _AssessmentScreenState extends State<AssessmentScreen> {
  String? _token;
  InAppWebViewController? _webController;

  @override
  void initState() {
    super.initState();
    mintEmbedToken(widget.assessmentId, widget.learnerRef).then((token) {
      setState(() => _token = token);
    });
  }

  String _buildHtml(String token) => '''
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
  <div id="root"></div>
  <script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.5/dist/umd/index.global.js"></script>
  <script>
    EdpireSDK.EdpireAssessment.mount({
      token: "$token",
      container: "#root",
      onComplete: function(r) {
        window.flutter_inappwebview.callHandler("onComplete", r);
      },
      onError: function(e) {
        window.flutter_inappwebview.callHandler("onError", e);
      }
    });
  </script>
</body>
</html>
''';

  @override
  Widget build(BuildContext context) {
    if (_token == null) return const Center(child: CircularProgressIndicator());

    return InAppWebView(
      initialData: InAppWebViewInitialData(data: _buildHtml(_token!)),
      onWebViewCreated: (controller) {
        _webController = controller;
        controller.addJavaScriptHandler(
          handlerName: 'onComplete',
          callback: (args) {
            final result = args.first as Map<String, dynamic>;
            debugPrint('Score: ${result['score']} / ${result['max_score']}');
            // Navigate to your results screen
          },
        );
        controller.addJavaScriptHandler(
          handlerName: 'onError',
          callback: (args) {
            debugPrint('Embed error: ${args.first}');
          },
        );
      },
    );
  }
}
```

***

## Native iOS / Android

The same HTML template works in any native WebView:

* **iOS:** `WKWebView` — use `loadHTMLString(_:baseURL:)` and receive messages via `WKScriptMessageHandler`
* **Android:** `WebView` — use `loadDataWithBaseURL()` and receive messages via `addJavascriptInterface`

The pattern is identical in both cases: your backend mints the token, the native code builds the HTML string with the token injected, and the result comes back via the WebView message bridge.

***

## Platform comparison

| Platform          | Server-side API                       | Player delivery        |
| ----------------- | ------------------------------------- | ---------------------- |
| React Native      | `EdpireClient` (npm) or plain `fetch` | `react-native-webview` |
| Flutter           | `http` / `dio` (Dart)                 | `flutter_inappwebview` |
| iOS native        | `URLSession` (Swift)                  | `WKWebView`            |
| Android native    | `OkHttp` (Kotlin)                     | `WebView`              |
| Ionic / Capacitor | `EdpireClient` (npm)                  | Full SDK in WebView    |

<Tip>
  Ionic and Capacitor apps run in a WebView already. You can use the full `@edpire/sdk` npm package directly — no need for the HTML template approach.
</Tip>
