Support livechat in a webview

Note:

Support Integration

The support integration is the component that allows customers to communicate with support operators. Before embedding a support livechat, make sure that you receive your livechat-id and tenant-id from our support.

The support integration in a webview happens by opening the WebView with a preconfigured URL. This will automatically create a support conversation when loaded.

Livechat URL structure

`https://${tenantId}-production-messaging-webchat.patty-awseuc1.swops.cloud/${livechatId}/`

To configure the instance, you can provide preconditions, language and other options as query parameters. The complex values have to be stringified and URL encoded. For example to pass the preconditions {"foo": "bar"}, append a query parameter preconditions=%7B%22foo%22%3A%22bar%22%7D.

The supported query parameters are preconditions, language and options

Example code to build a embeddable chat URL in JavaScript

const language = 'fr';
const preconditions = encodeURIComponent(JSON.stringify({
  language,
  zip: '1845'
}));
const options = encodeURIComponent(JSON.stringify({
  captureLinks: true
}));

const url = `https://${tenantId}-production-messaging-webchat.patty-awseuc1.swops.cloud/${livechatId}/` +
  `?language=${language}` +
  `&preconditions=${preconditions}` +
  `&options=${options}`;

If the livechat is embedded via URL, we emit the following events using the postMessage API. Messages are serialized as JSON objects.

Message
type MessageEventData =
  {
    type: string;
    data: {[key: string]: any};
    _by: 'smoope';
  } &
  (
    | ReadyToSupportData
    | CapturedLinkData
    | RoomClosedData
    | SupportRequestRejectedData
    | RequestTokenData
  );

// Example MessageEvent
// {"type":"ReadyToSupport","data":{"room":"01CXWBKT6Q6YCV1X12FNSJ0QDR"},"_by":"smoope"}

ReadyToSupport

interface ReadyToSupportData {
  type: 'ReadyToSupport';
  data: { room: string }
}

CapturedLink

interface CapturedLinkData {
  type: 'CapturedLink';
  data: { url: string }
}

RoomClosed

interface RoomClosedData {
  type: 'RoomClosed';
  data: {}
}

SupportRequestRejected

interface SupportRequestRejectedData {
  type: 'SupportRequestRejected';
  data: {
    title: string;
    description: string;
    reason: 'NO_OPERATOR_AVAILABLE' | 'OUTSIDE_BUSINESS_HOURS' | 'SERVER_ERROR'
  }
}

RequestToken

interface RequestTokenData {
  type: 'RequestToken';
  data: {};
}

We emit the following types via postMessage.

  • ReadyToSupport, if the chat is initialized far enough to accept support input from the user

  • CapturedLink, if the captureLinks option is enabled with {url: string} as event payload

  • RoomClosed, if the current support room is closed

  • SupportRequestRejected, if the current support request got rejected

  • RequestTokenData, if the provided token became invalid

Due to missing bridge between JavaScript and Android/iOS, we have to write some custom code to listen for emitted events. (See "Android Setup" and "iOS Setup")

Android Setup

In Android, you have to define a JavaScript interface to inject into the WebView.

Kotlin

class SmoopeIpc {
  @JavascriptInterface
  fun postMessage(data: String, origin: String) {
    // handle livechat event
    println("Smoope chat emitted data: $data $origin")
  }
}

Java

class SmoopeIpc {
  @JavascriptInterface
  public void postMessage(String data, String origin) {
    // handle livechat event
    System.out.println("Smoope chat emitted data: " + data + " " + origin);
  }
}

Using the addJavascriptInterface, register your Interface as SMOOPE_IPC. This allows us to call your Interface, if defined.

Kotlin

webView.settings.javaScriptEnabled = true
webView.webViewClient = WebViewClient()
webView.addJavascriptInterface(SmoopeIpc(), "SMOOPE_IPC")
webView.loadUrl("<webchat-url>")

Java

webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.addJavascriptInterface(new SmoopeIpc(), "SMOOPE_IPC");
webView.loadUrl("<webchat-url>");
Note:

File upload

If you want to allow the user to select a file, you have to make some adjustments. First set a WebChromeClient on the WebView instance.

Next you can implement the onShowFileChooser to fit your use-case.

iOS Setup

Similar to Android, in iOS, you have to register a listener for a custom messageHandler method on a WKWebView. Use the SMOOPE_IPC name to add to the WKUserContentController.

Setup the WKWebView to receive livechat events:

Swift

let config = WKWebViewConfiguration()
config.preferences.javaScriptEnabled = true
let userContentController = WKUserContentController()
userContentController.add(self, name: "SMOOPE_IPC")
config.userContentController = userContentController

webView.load(URLRequest(url: URL(string: "<webchat-url>")!))

Objective-C

 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[[config preferences] setJavaScriptEnabled:true];
WKUserContentController * userCOntentController = [[WKUserContentController alloc] init];
[userCOntentController addScriptMessageHandler:self name:@"SMOOPE_IPC"];
[config setUserContentController:userCOntentController];

[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"<webchat-url>"]]];

Listen for messages with the SMOOPE_IPC name:

Swift

extension ViewController: WKScriptMessageHandler {
  func userContentController(_
    userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
  ) {
    if message.name == "SMOOPE_IPC", let messageBody = message.body as? String {
      // handle livechat event
      print(messageBody)
    }
  }
}

Objective-C

@interface ViewController (ScriptMessageHandler) <WKScriptMessageHandler>
@end

@implementation ViewController (ScriptMessageHandler)

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
  NSString *messageBody = [message body];
  if ([[message name] isEqualToString:@"SMOOPE_IPC"] && [messageBody isKindOfClass:[NSString class]]) {
    // handle livechat event
    NSLog(@"%@", messageBody);
  }
}

@end

Authenticating a user

In cases where you have an authenticated user and want to pass its identity to our system, you have to provide us with the token.

First you have to tell the app that it should take a token from the outside by loading it with the needsAuthentication query parameter set to true (needsAuthentication=true).

In this configuration, the chat will emit a RequestToken and wait for the provided token by the RequestTokenResponse message from its host.

Kotlin

val myNewValidToken = "0xDEADBEEF"
val data = gson.toJson(
  mapOf(
    "type" to "RequestTokenResponse",
    "data" to mapOf("token" to myNewValidToken),
    "_by" to "smoope"
  )
)
webView.postWebMessage(WebMessage(data), Uri.parse("*"))

Java

var myNewValidToken = "0xDEADBEEF";
var data = gson.toJson(
  new HashMap() {{
    put("type", "RequestTokenResponse");
    put("data", new HashMap() {{
      put("token", myNewValidToken);
    }});
    put("_by", "smoope");
  }}
);
webView.postWebMessage(new WebMessage(data), Uri.parse("*"));

RequestTokenResponse type

interface RequestTokenResponseData {
  type: 'RequestTokenResponse';
  data: { token: string };
  _by: 'smoope';
}

If the provided token expires, the chat emits a RequestToken event which allows you to refresh the token. To do that, simply post a RequestTokenResponse message with the new token.

Posting a message from iOS

Because iOS exposes no way to communicate from iOS context to the WKWebview context, you have to make additional adjustments.

If the app is launched inside a WKWebview, we register a global wkSmoopePostMessage method. This allows you to use the evaluateJavaScript method, to emit data to the chat.

The method accepts a JSON-Object which contains the message information.

Responding to a token request with a new token

let object: [String : Any] = [
    "type": "RequestTokenResponse",
    "data": ["token": myNewValidToken],
    "_by": "smoope"
]
let string = String(
    data: try! JSONSerialization.data(withJSONObject: object),
    encoding: .utf8
)!
self.webview?.evaluateJavaScript("wkSmoopePostMessage(\(string))")