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/

ParameterRequiredDescription
graphcomment_idYour site's shortname
urlCanonical URL of the page/content (URL-encoded)
uidUnique identifier of the thread/content
page_titleTitle of the article (URL-encoded)
inappAlways true for a standalone WebView
redirectAlways true, to receive gc_url
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:

{
  "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). 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 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:

EventFires whenWhat your app should do
gc-readyWebView is readyInject gcSsoLogin(ssoData)
gc-loadedComments are loadedInject gcSsoLogin(ssoData)
gc-sso-authThe 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-conflictUsername conflict detectedShow 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):

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

React Native

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)

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)

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

SymptomLikely causeFix
No login after injectiongcSsoLogin called before the widget was readyWait for the gc-loaded / gc-ready event
Login fails intermittently (slow networks)ssoData fetched early, expired (~5 min) by the time it was injectedFetch the ssoData after gc-ready / gc-loaded, then inject immediately
Third-party cookie problemsNormal in a WebViewUse only the ssoData flow — no cookies involved
Persistent username conflictssoData regenerated with the same usernameRegenerate server-side with a corrected username
Injection does nothing — no login, no network call, no errorapi_public_key missing from the URLCheck that &api_public_key= was appended to gc_url
No events receivedBridge script not installed, or installed after the page's own scripts ranInstall the message listener with your platform's injection mechanism, as in the examples (injectedJavaScript, WKUserScript, onPageFinished)
Session lost on every app launchDOM storage disabled in the WebViewKeep 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


Did this page help you?