---
updatedAt: 2026-07-31T11:52:43.000Z
agentTools:
  projectIndex: https://developers.graphcomment.com/llms.txt
---

# Mobile (WebView)

Embed GraphComment in a native iOS, Android or React Native app — one WebView, two steps, SSO included.

In a native app, GraphComment loads in a **standalone WebView** — no iframe, no embed script. One flow covers React Native, Android and iOS, in two steps:

1. Ask the API for the thread's URL (`thread-load`), and load it in your WebView.
2. If you use SSO, inject the user's authentication with `window.gcSsoLogin(ssoData)`.

Full native examples for all three platforms are further down this page.

## Step 1 — Get the thread URL

**`GET /api/pub/thread-load/`**

| Parameter         | Required | Description                                     |
| ----------------- | -------- | ----------------------------------------------- |
| `graphcomment_id` | ✅        | Your site's shortname                           |
| `url`             | ✅        | Canonical URL of the page/content (URL-encoded) |
| `uid`             | ✅        | Unique identifier of the thread/content         |
| `page_title`      | ✅        | Title of the article (URL-encoded)              |
| `inapp`           | ✅        | Always `true` for a standalone WebView          |
| `redirect`        | ✅        | Always `true`, to receive `gc_url`              |

```bash
curl "https://api.graphcomment.com/api/pub/thread-load/\
?graphcomment_id=your-shortname\
&url=http://example.com/foo/bar15\
&page_title=My%20article\
&uid=content18\
&inapp=true\
&redirect=true"
```

**Response:**

```json
{
  "gc_url": "https://graphcomment.com/front/?website_id=your-shortname&url=http://example.com/foo/bar15&uid=content18&..."
}
```

> ⚠️ One mandatory addition
> Before loading `gc_url` in your WebView, append **`&api_public_key=<your SSO public key>`** to it (the SSO public key from your back-office, **Settings → Authentication → Unidirectional SSO** — the same key described in [Single Sign-On](./authentication-sso)). Without it, the widget still loads and its events still fire — but every `gcSsoLogin` injection is **silently ignored**: no network call, no error, the user is never signed in.

The final URL you load:

```
https://graphcomment.com/front/?website_id=your-shortname&...&api_public_key=your-api-public-key
```

## Step 2 — SSO inside the WebView

Skip this step if you don't use single sign-on — the widget then works with GraphComment's own accounts.

With SSO, authentication happens **only by JavaScript injection**, never through the URL:

> ❗ Absolute security rule
> `ssoData` must **never** appear in a URL. Inject it with `window.gcSsoLogin(ssoData)` once the page is loaded.

Your server generates the `ssoData` string exactly as for the web — see [Single Sign-On](./authentication-sso) for the format and the Node/PHP/Python snippets. Your app fetches it from your backend, then injects it when the WebView signals it's ready.

**Events your app receives from the WebView:**

| Event             | Fires when                                                                                                    | What your app should do                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `gc-ready`        | WebView is ready                                                                                              | Inject `gcSsoLogin(ssoData)`                               |
| `gc-loaded`       | Comments are loaded                                                                                           | Inject `gcSsoLogin(ssoData)`                               |
| `gc-sso-auth`     | The widget asks your app for a login (only on a few paths — e.g. guest flows; **not** on every "Sign in" tap) | Fetch a fresh `ssoData` from your server and re-inject     |
| `gc-sso-conflict` | Username conflict detected                                                                                    | Show your resolution UI → regenerate `ssoData` → re-inject |

> 📘 Don't wait for `gc-sso-auth`
> `gc-ready` / `gc-loaded` are your main injection points. Keep handling `gc-sso-auth` defensively, but don't rely on it as your re-login trigger: it does not fire on every "Sign in" tap nor when a session expires. Re-inject a fresh `ssoData` whenever your app's own login state changes.

**Functions available inside the WebView** — defined by the JavaScript bridge your app injects (each example below installs them; the widget page does not create them itself):

| Function                     | Role                                                      |
| ---------------------------- | --------------------------------------------------------- |
| `window.gcSsoLogin(ssoData)` | Signs the user in (inject after `gc-loaded` / `gc-ready`) |
| `window.gcSsoLogout()`       | Signs the SSO user out                                    |

## React Native

```tsx
import React, { useRef, useEffect } from "react";
import { WebView } from "react-native-webview";

async function fetchThreadUrl() {
  const res = await fetch(
    "https://api.graphcomment.com/api/pub/thread-load/" +
    "?graphcomment_id=your-shortname&url=http://example.com/foo/bar15" +
    "&page_title=My%20article&uid=content18&inapp=true&redirect=true"
  );
  const { gc_url } = await res.json();
  return gc_url + "&api_public_key=your-api-public-key";
}

async function fetchSsoData() {
  const res = await fetch("https://your-api/sso-data", { credentials: "include" });
  const { ssoData } = await res.json();
  return ssoData;
}

export default function GraphCommentScreen() {
  const webRef = useRef<WebView>(null);

  useEffect(() => {
    (async () => {
      const gcUrl = await fetchThreadUrl();
      webRef.current?.loadUrl?.(gcUrl);
    })();
  }, []);

  const injectedJS = `
    (function() {
      window.gcSsoLogin = function(ssoData) {
        window.postMessage(JSON.stringify({ info: "sso-login", data: ssoData }), "*");
      };
      window.gcSsoLogout = function() {
        window.postMessage(JSON.stringify({ info: "sso-logout" }), "*");
      };
      const rnPost = window.ReactNativeWebView &&
        window.ReactNativeWebView.postMessage.bind(window.ReactNativeWebView);
      window.addEventListener("message", function(ev) {
        try {
          const p = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data;
          if (p && p.name && rnPost) rnPost(JSON.stringify(p));
        } catch(e){}
      }, false);
    })();
    true;
  `;

  const onMessage = async (event) => {
    const msg = JSON.parse(event.nativeEvent.data);
    switch (msg.name) {
      case "gc-loaded":
      case "gc-ready":
      case "gc-sso-auth":
        const ssoData = await fetchSsoData();
        webRef.current?.injectJavaScript(
          `window.gcSsoLogin(${JSON.stringify(ssoData)}); true;`
        );
        break;
      case "gc-sso-conflict":
        // Resolve the conflict in your app, then regenerate a ssoData
        break;
    }
  };

  return (
    <WebView
      ref={webRef}
      source={{ uri: "about:blank" }}
      injectedJavaScript={injectedJS}
      onMessage={onMessage}
      javaScriptEnabled
      domStorageEnabled
    />
  );
}
```

## Android (Kotlin)

```kotlin
val webView: WebView = findViewById(R.id.webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true

webView.addJavascriptInterface(object {
  @JavascriptInterface
  fun postMessageFromGC(payload: String) {
    val name = JSONObject(payload).optString("name")
    when (name) {
      // Fetch the ssoData only ONCE the widget is ready: a ssoData fetched
      // too early can expire (~5 min) before it is injected.
      "gc-loaded", "gc-ready", "gc-sso-auth" -> {
        val ssoData = fetchSsoDataFromApi()
        webView.post {
          webView.evaluateJavascript("window.gcSsoLogin(${JSONObject.quote(ssoData)});", null)
        }
      }
      "gc-sso-conflict" -> {
        // Resolve the conflict in your app, then regenerate a ssoData
      }
    }
  }
}, "GCNativeBridge")

val injected = """
  (function() {
    window.gcSsoLogin = function(ssoData) {
      window.postMessage(JSON.stringify({ info: "sso-login", data: ssoData }), "*");
    };
    window.gcSsoLogout = function() {
      window.postMessage(JSON.stringify({ info: "sso-logout" }), "*");
    };
    window.addEventListener("message", function(ev) {
      try {
        var p = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data;
        if (p && p.name) window.GCNativeBridge.postMessageFromGC(JSON.stringify(p));
      } catch(e){}
    }, false);
  })();
""".trimIndent()

// Load the gc_url obtained from thread-load + &api_public_key=...
val gcUrl = fetchThreadUrlFromApi()
webView.loadUrl(gcUrl)

webView.webViewClient = object : WebViewClient() {
  override fun onPageFinished(view: WebView, url: String) {
    view.evaluateJavascript(injected, null)  // install the bridge; injection happens on gc-ready/gc-loaded
  }
}
```

## iOS (Swift + WKWebView)

```swift
let config = WKWebViewConfiguration()
config.preferences.javaScriptEnabled = true
let webView = WKWebView(frame: .zero, configuration: config)

let script = """
  (function(){
    window.gcSsoLogin = function(ssoData){
      window.postMessage(JSON.stringify({ info: "sso-login", data: ssoData }), "*");
    };
    window.gcSsoLogout = function(){
      window.postMessage(JSON.stringify({ info: "sso-logout" }), "*");
    };
    window.addEventListener("message", function(ev){
      try {
        var p = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data;
        if (p && p.name) window.webkit.messageHandlers.gcBridge.postMessage(JSON.stringify(p));
      } catch(e){}
    }, false);
  })();
"""
let userScript = WKUserScript(
  source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true
)
webView.configuration.userContentController.addUserScript(userScript)
webView.configuration.userContentController.add(self, name: "gcBridge")

extension YourVC: WKScriptMessageHandler {
  func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
  ) {
    guard message.name == "gcBridge",
          let text = message.body as? String,
          let data = text.data(using: .utf8),
          let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
          let name = payload["name"] as? String else { return }

    switch name {
      case "gc-loaded", "gc-ready", "gc-sso-auth":
        let ssoData = fetchSsoData()
        let js = "window.gcSsoLogin(\(String(describing: ssoData)));"
        webView.evaluateJavaScript(js, completionHandler: nil)
      case "gc-sso-conflict":
        // Resolve the conflict in your app
        break
      default: break
    }
  }
}

// Load the URL returned by thread-load + &api_public_key=...
let gcUrl = URL(string: fetchThreadUrlFromApi())!
webView.load(URLRequest(url: gcUrl))
```

## Troubleshooting

| Symptom                                                      | Likely cause                                                               | Fix                                                                                                                                                |
| ------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| No login after injection                                     | `gcSsoLogin` called before the widget was ready                            | Wait for the `gc-loaded` / `gc-ready` event                                                                                                        |
| Login fails intermittently (slow networks)                   | `ssoData` fetched early, expired (\~5 min) by the time it was injected     | Fetch the `ssoData` **after** `gc-ready` / `gc-loaded`, then inject immediately                                                                    |
| Third-party cookie problems                                  | Normal in a WebView                                                        | Use only the `ssoData` flow — no cookies involved                                                                                                  |
| Persistent username conflict                                 | `ssoData` regenerated with the same username                               | Regenerate server-side with a corrected username                                                                                                   |
| Injection does nothing — no login, no network call, no error | `api_public_key` missing from the URL                                      | Check that `&api_public_key=` was appended to `gc_url`                                                                                             |
| No events received                                           | Bridge script not installed, or installed after the page's own scripts ran | Install the message listener with your platform's injection mechanism, as in the examples (`injectedJavaScript`, `WKUserScript`, `onPageFinished`) |
| Session lost on every app launch                             | DOM storage disabled in the WebView                                        | Keep `domStorageEnabled` on — in standalone mode the widget persists its session token in `localStorage`                                           |

## Delivery checklist

* [ ] Call `GET /api/pub/thread-load/` to obtain `gc_url`
* [ ] Append `&api_public_key=<key>` to `gc_url` before loading
* [ ] Load the final URL in the WebView
* [ ] Inject `gcSsoLogin(ssoData)` after `gc-loaded` / `gc-ready`
* [ ] Handle `gc-sso-auth` (re-fetch `ssoData` + re-inject)
* [ ] Handle `gc-sso-conflict` (resolution UI + new `ssoData`)
* [ ] Never put `ssoData` in a URL

## Next steps

* **[Single Sign-On](./authentication-sso)** — how your server generates `ssoData`.
* **[Widget configuration](./widget-configuration)** — options reference (note `behaviour.inapp`).