purserDOCS

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 客服页)).

https://askpurser.com/c/<publishable key>?vid=<visitor ID>&lang=en#token=<identity token>
PartMeaning
<publishable key>Your workspace's publishable key, starting with pk_. An unknown key returns 404
vidVisitor 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
langInterface 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=1Optional. 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

  1. When the customer opens support in your app, the app asks your server for an identity token and sends the vid along.
  2. Your server signs the token with the workspace's identity signing secret, with visitorId set to that vid. Signing works exactly as for the website; see Identity tokens.
  3. 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:

PlatformBridgeWhat the page sends
iOS (WKWebView)webkit.messageHandlers.purser{ type: "close" }
Android (WebView)PurserAndroid.close()Calls close() directly
React Native (react-native-webview)ReactNativeWebView.postMessageThe string {"type":"purser:close"}
Flutter (webview_flutter)JavaScript channel PurserThe 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.

On this page