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:
- Ask the API for the thread's URL (
thread-load), and load it in your WebView. - 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 |
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 additionBefore loading
gc_urlin 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 everygcSsoLogininjection 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
ssoDatamust never appear in a URL. Inject it withwindow.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:
| 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 forgc-sso-auth
gc-ready/gc-loadedare your main injection points. Keep handlinggc-sso-authdefensively, 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 freshssoDatawhenever 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
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
| 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 obtaingc_url - Append
&api_public_key=<key>togc_urlbefore loading - Load the final URL in the WebView
- Inject
gcSsoLogin(ssoData)aftergc-loaded/gc-ready - Handle
gc-sso-auth(re-fetchssoData+ re-inject) - Handle
gc-sso-conflict(resolution UI + newssoData) - Never put
ssoDatain a URL
Next steps
- Single Sign-On — how your server generates
ssoData. - Widget configuration — options reference (note
behaviour.inapp).
Updated about 1 month ago
