In-app support page
Open the same AI support chat window inside your iOS, Android, React Native or Flutter app with a WebView.
Your app does not need an SDK: open a web page in a WebView and you get the same chat window as on your website, with the same theme, knowledge and inbox. Your full link is on the Setup (接入) page of the console, in "5. Add it to your app (H5 support page)" (5. 装到你的 App(H5 客服页)).
Link format
https://askpurser.com/c/<publishable key>?vid=<visitor ID>&lang=en#token=<identity token>| Part | Meaning |
|---|---|
<publishable key> | Your workspace's publishable key, starting with pk_. An unknown key returns 404 |
vid | Visitor ID. Your app generates a UUID once, stores it on the device and sends the same one every time. Without it the page makes its own inside the WebView, but then you cannot sign an identity token for it |
lang | Interface language, e.g. zh-CN or ja. Without it, the WebView's system language is used |
#token= | The signed-in customer's identity token. Leave it out when nobody is signed in; the chat is then anonymous |
close=1 | Optional. If your app installs none of the native bridges below, this shows a close button that goes back to the previous page |
The in-app page is not restricted by Allowed websites (允许的网站); it works in a real workspace without setting any.
Signed-in customers
- When the customer opens support in your app, the app asks your server for an identity token and sends the
vidalong. - Your server signs the token with the workspace's identity signing secret, with
visitorIdset to thatvid. Signing works exactly as for the website; see Identity tokens. - The app opens the page with the token after
#token=.
The part after # is never sent to any server, and the page wipes it from the address bar as soon as it reads it. A token can be used once and is valid for 10 minutes at most (the SDK signs for 5 minutes by default), so sign a new one every time you open the page. An invalid token makes the chat anonymous without an error; no token means a new anonymous conversation that never continues the previous customer's.
Instead of the link, you can also set window.__PURSER_IDENTITY__ = "<token>" before the page's script runs (for example with a WKUserScript injected at document start on iOS).
Testing in a browser
Navigating to a link that differs only after the # does not reload the page. To test a different token, go to another page first, then open the new link.
Closing the page
The close button in the top right tells your app through a native bridge you install. The page looks for these bridges in this order and uses the first one it finds:
| Platform | Bridge | What the page sends |
|---|---|---|
| iOS (WKWebView) | webkit.messageHandlers.purser | { type: "close" } |
| Android (WebView) | PurserAndroid.close() | Calls close() directly |
| React Native (react-native-webview) | ReactNativeWebView.postMessage | The string {"type":"purser:close"} |
| Flutter (webview_flutter) | JavaScript channel Purser | The string close |
| Flutter (flutter_inappwebview) | callHandler("purser", "close") | The argument close |
The page checks for a bridge when it loads, so install the bridge before you load the page. With no bridge the close button is hidden (unless the link has close=1).
iOS (WKWebView)
import UIKit
import WebKit
final class SupportViewController: UIViewController, WKScriptMessageHandler {
var chatURL: URL! // https://askpurser.com/c/pk_…?vid=…&lang=…#token=…
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
// Register before loading the page, or it hides the close button
config.userContentController.add(self, name: "purser")
let webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
webView.load(URLRequest(url: chatURL))
}
func userContentController(_ controller: WKUserContentController,
didReceive message: WKScriptMessage) {
if let body = message.body as? [String: Any], body["type"] as? String == "close" {
dismiss(animated: true)
}
}
}On iOS, customers can pick and send photos from the chat window with no extra work.
Android (WebView)
class SupportActivity : AppCompatActivity() {
private var fileCallback: ValueCallback<Array<Uri>>? = null
private val pickImage =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
fileCallback?.onReceiveValue(uri?.let { arrayOf(it) } ?: emptyArray())
fileCallback = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
// Inject before loadUrl
webView.addJavascriptInterface(object {
@JavascriptInterface
fun close() = runOnUiThread { finish() }
}, "PurserAndroid")
// Let customers send photos: a WebView shows no file picker by default
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
view: WebView,
callback: ValueCallback<Array<Uri>>,
params: FileChooserParams,
): Boolean {
fileCallback = callback
pickImage.launch("image/*")
return true
}
}
webView.loadUrl(intent.getStringExtra("chatUrl")!!)
}
}If your Android WebView does not implement onShowFileChooser, the chat window's Add a photo button does nothing.
React Native (react-native-webview)
import { WebView } from "react-native-webview";
export function SupportScreen({ navigation, chatUrl }) {
return (
<WebView
source={{ uri: chatUrl }}
// Setting onMessage is what gives the page the ReactNativeWebView bridge
onMessage={(e) => {
try {
if (JSON.parse(e.nativeEvent.data).type === "purser:close") navigation.goBack();
} catch {}
}}
/>
);
}Flutter (webview_flutter)
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('Purser', onMessageReceived: (m) {
if (m.message == 'close') Navigator.pop(context);
})
..loadRequest(Uri.parse(chatUrl));With flutter_inappwebview, register a JavaScript handler named purser and close the page when it receives the argument close.
Same as the website
- The window opens with the AI disclosure greeting; AI replies are labelled "AI assistant" and your team's replies "Customer service".
- The interface comes in 12 languages, and the AI answers in the language of the customer's latest message.
- In a test workspace the title shows "Test mode".
- The page keeps clear of the notch and the home indicator (safe areas), so you do not need to add margins.
More: Website chat.