ガイド

アプリ内チャット(WebView)

WebView を使って、チャットウィジェットを機能そのままにモバイル/デスクトップアプリへ組み込めます。更新も自動です。

ネイティブアプリにカスタマーサポートを組み込む最短の方法は、当社がホストしている全画面ウィジェットページをWebViewで読み込むことです。Web サイトとまったく同じウィジェット(ホーム、メッセージ、ヘルプ記事、検索、絵文字、添付ファイル、タイムスタンプ、既読表示、会話の終了)がそのまま使え、しかも自動で更新されます。一度組み込めば、ウィジェットが変わってもアプリを再リリースする必要はありません。

クイックスタート

WebView でこの URL を開きます。ログイン済みのユーザーなら必ずexternalIdとemail(できればnameも)を付けてください。付けないと、担当者側にはいつまでも匿名の訪問者としか表示されません。値はすべて URL エンコードします。

https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID&externalId=USER_ID&email=USER_EMAIL&name=USER_NAME

匿名の形式に切り替えるのは、ユーザーがログインしていないときだけです。

https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID

⚠️ 担当者の画面に「匿名の訪問者」と表示され、メールアドレスがない?組み込みで最も多いミスです。WebView の URL にemail/externalIdが入っていません。サーバー側でこれらを作り出すことはできないため、アプリから渡す必要があります。訪問者 ID がanon-…のような形なら、何も渡されていません。

JavaScript / 任意の WebView(Flutter 以外)

Electron、デスクトップのシェル、React Native、あるいは URL を自分で設定するネイティブのWKWebViewなら、JS で URL を組み立てます。URLSearchParamsがすべての値を自動でエンコードしてくれます。

// Build the support URL for the signed-in user before opening the WebView.
function buildSupportUrl(domain, user) {
  const params = new URLSearchParams({
    appId: 'YOUR_APP_ID',
    platform: 'windows',            // Strongly recommended: decides which help articles this visitor sees
                                    // ios / macos_appstore / macos / android / windows / web
    // locale: userSelectedLanguage, // Optional: only if your app has its own language switch; also add forceLocale: '1'
                                    // Omitted = follow the device language (covers the App Store's 50 localizations)
    // — Always pass these for signed-in users, or agents only see an "anonymous visitor" —
    externalId: user.id,            // The user's unique ID in your system
    email: user.email,              // The user's email (important)
    name: user.name || '',          // The user's name (optional)
    // hmac: user.hmac,             // Optional: computed on your server to prevent impersonation (see HMAC below)
    // attrs: JSON.stringify({ plan: user.plan, expiresAt: user.expireAt }), // Optional: custom attributes
  });
  return `https://${domain}/api/widget-embed?${params.toString()}`;
}

// Usage: load the returned url in your WebView (instead of the old ?appId=...-only URL).
const url = buildSupportUrl('askais.com', currentUser);
myWebView.loadURL(url); // Electron: win.loadURL(url); native: load this URL

Flutter(webview_flutter)

pubspec.yamlにwebview_flutter: ^4.xを追加し、次のように書きます。

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class SupportPage extends StatefulWidget {
  const SupportPage({super.key});
  @override
  State<SupportPage> createState() => _SupportPageState();
}

class _SupportPageState extends State<SupportPage> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    // For signed-in users always include externalId + email, or agents see an "anonymous visitor" (no name / email).
    // Pass only appId + locale when the user is not signed in. Uri.https URL-encodes the values.
    final uri = Uri.https('askais.com', '/api/widget-embed', {
      'appId': 'YOUR_APP_ID',
      'platform': Platform.isIOS ? 'ios' : 'android', // Strongly recommended: decides which help articles are visible
      // No locale = follow the device language (covers the App Store's 50 localizations).
      // Only pass it if your app has its own language switch, together with 'forceLocale': '1':
      // 'locale': appSettings.selectedLanguage,
      'externalId': user.id, // The user's unique ID in your system (important)
      'email': user.email, // The user's email (important)
      'name': user.name, // Optional
      // 'hmac': hmacFromYourServer, // Optional: computed on your server to prevent impersonation
    });
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.white)
      ..loadRequest(uri);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Support')),
      // Full-screen: drop the appBar and use your own close button.
      body: SafeArea(child: WebViewWidget(controller: _controller)),
    );
  }
}

// Open it from wherever your "Support" button lives:
// Navigator.push(context,
//   MaterialPageRoute(builder: (_) => const SupportPage()));

ほかの技術スタックの iOS/Android WebView(Swift のWKWebView、Kotlin のWebView、React Native のreact-native-webview)でも考え方は同じです。URL を読み込み、JavaScript を有効にするだけです。

ネイティブ iOS と Android

インストールする SDK はありません。同じ URL を組み立てて WebView に渡すだけです。Mac には注意が必要です。App Store 版と DMG 版は同じ OS 上で動くため、この値は実行時の判定ではなく、ビルドフラグで決める必要があります。

// ── iOS (Swift / WKWebView) ────────────────────────────────
// On Mac Catalyst / macOS, set platform to macos_appstore or macos.
#if targetEnvironment(macCatalyst)
let platform = "macos_appstore"   // App Store build; a DMG build from your website passes "macos"
#else
let platform = "ios"              // iPhone and iPad are both ios
#endif

var comps = URLComponents(string: "https://\(domain)/api/widget-embed")!
comps.queryItems = [
    .init(name: "appId", value: "YOUR_APP_ID"),
    .init(name: "platform", value: platform),
    .init(name: "externalId", value: user.id),     // Required for signed-in users
    .init(name: "email", value: user.email),       // Required for signed-in users
    .init(name: "name", value: user.name),
    // Only pass these two if your app has its own language switch:
    // .init(name: "locale", value: settings.language),
    // .init(name: "forceLocale", value: "1"),
]
webView.load(URLRequest(url: comps.url!))          // URLComponents URL-encodes the values


// ── Android (Kotlin / WebView) ─────────────────────────────
val url = Uri.parse("https://$domain/api/widget-embed")
    .buildUpon()
    .appendQueryParameter("appId", "YOUR_APP_ID")
    .appendQueryParameter("platform", "android")
    .appendQueryParameter("externalId", user.id)
    .appendQueryParameter("email", user.email)
    .appendQueryParameter("name", user.name)
    // .appendQueryParameter("locale", settings.language)
    // .appendQueryParameter("forceLocale", "1")
    .build()

webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true          // The widget uses localStorage to remember the visitor
webView.loadUrl(url.toString())

URL パラメーター

  • appId (必須):受信トレイの App ID。
  • locale:UI の言語。ほとんどの場合、指定しないでください。指定しなければ、ウィジェットは WebView が伝えるデバイスの言語に従います。ユーザーが期待するのもこの動作です。UI は App Store Connect の 50 のローカリゼーションすべてに翻訳済みで、それ以外の言語は英語で表示されます。指定するのは、アプリに独自の言語切り替えがある場合だけです。その場合はユーザーが選んだ言語を渡し、デバイスの設定より優先されるようforceLocale=1も付けます。値を固定で書き込むのは厳禁です。zh-Hansを固定していると、日本語アプリに中国語のウィジェットが表示される、といったことが起こります。
  • platform (強く推奨):訪問者が使っているビルド。どのヘルプ記事を表示するか、AI が App Store のルールに沿って回答するかどうかがこれで決まります。省略した場合は WebView の User-Agent から推測します。iPhone、Android、Windows は判別できますが、Mac App Store 版と DMG 版の区別や、デスクトップモードの iPad は判別できません。値:ios(iPhone と iPad)、macos_appstore(Mac App Store 版)、macos(DMG で直接配布する版)、android、windows、web。
  • externalId:アプリ内でユーザーを一意に識別する ID。渡すと、担当者がそのユーザーを識別でき、過去の会話履歴もまとめられます。
  • email、name、avatarUrl:担当者に表示されるプロフィール。
  • hmac:本人確認用の署名(下記参照)。任意。

プラットフォームごとにコンテンツを隠す(App Store 対応)

App Store のガイドライン 3.1.1 では、iOS アプリ内で外部での購入、サブスクリプション、紹介報酬に関するコンテンツを表示できません。そのため、ヘルプ記事とよくある質問の各項目には、プラットフォームごとの表示設定があります。ダッシュボードでヘルプセンターまたはよくある質問のページを開き、その項目のプラットフォームのチップを切り替えてください。選択したプラットフォームの訪問者には一覧に表示されず、直接リンクでも開けません。

AI はヘルプ記事やよくある質問をもとに回答するのではなく、「Training」に追加した内容をもとに回答します。そこにあるファイルと Q&A にも同じプラットフォームのチップがあり、あるプラットフォームで非表示にした項目は、そのプラットフォームの訪問者への回答には使われません。AI にも触れさせたくない場合は、両方で非表示にしてください。

これが確実に機能するかどうかは 2 つの点で決まります。まず、アプリが自分でplatformを送ること。User-Agent からの推測は予備の手段にすぎません。次に、その値が OS だけでなくビルドを反映していることです。

  • iPad は区別しません。同じ iOS アプリが同じルールのもとで動くため、iosで iPhone と iPad の両方をカバーします。
  • Mac にはビルドが 2 つあります。Mac App Store 版はガイドライン 3.1.1 の対象ですが、自社サイトで配布する DMG 版は対象外です。どちらも同じ OS なので、実行時に見分ける方法はありません。ビルド時に(たとえば Xcode のフラグで)決め、それに応じてmacos_appstoreまたはmacosを送ってください。間違えると、ストア版が審査中にサブスクリプション関連のコンテンツを見せてしまうか、DMG 版がヘルプセンターの半分を理由もなく隠してしまいます。

変更はすぐに反映されます。設定はアプリではなくダッシュボードにあるので、表示内容を変えるためにアプリをリリースする必要はありません。

本人確認(HMAC、任意)

externalIdが本当にログイン中のユーザーのものであることを証明し、なりすましを防ぐには、HMAC 署名を渡します。署名は必ず自社のサーバーで計算してください。シークレットキーをアプリに含めてはいけません。

  • hmac = HMAC-SHA256(secretKey, externalId)。小文字の 16 進数で出力します。
  • ペイロードはexternalIdなので、必ず一緒に渡してください。渡さないとウィジェットは独自の匿名 ID を使うため、emailだけに対する署名は拒否されます。
  • secretKeyは受信トレイのシークレットキーです(受信トレイごとに 1 つ。受信トレイの設定で確認できます)。hmacがない訪問者は、未確認として扱われます。
import { createHmac } from 'node:crypto';

// SECRET_KEY lives only on your server, never ship it inside the app.
const hmac = createHmac('sha256', SECRET_KEY)
  .update(externalId)   // exactly the externalId you pass to the widget
  .digest('hex');

// Return { externalId, email, hmac } to the app, which appends them to the URL.

プラットフォーム別の注意点

  • Android:INTERNET権限は残しておきます。戻るボタンを横取りし、まず WebView 内で前の画面に戻るようにしてください。最近のwebview_flutterは、添付ファイル用の<input type="file">に対応しています。
  • iOS:HTTPS なので ATS の例外設定は不要です。画像のアップロードはウィジェットの標準機能で、WKWebViewが自動でネイティブの写真ピッカーを開くため、アプリ側で作るものはありません。フォトライブラリからの選択に権限は要りません。「写真を撮る」のカメラ機能を使いたい場合に限り、Info.plistにNSCameraUsageDescriptionを追加してください。追加しないまま、ユーザーがそれをタップするとアプリがクラッシュします。

ネイティブ UI を使いたい場合

WebView ではなく、ネイティブのチャット画面を自作する必要がある場合は、モバイル SDKページにあるロジックのみのクライアントを使ってください。ほとんどのアプリでは、上記の WebView のほうが早くリリースでき、常にすべての機能を利用できます。