แชทในแอป (WebView)
ฝังวิดเจ็ตแชทแบบเต็มรูปแบบลงในแอปมือถือหรือเดสก์ท็อปใดก็ได้ด้วย WebView ใช้ได้ครบทุกฟีเจอร์และอัปเดตอัตโนมัติ
วิธีที่เร็วที่สุดในการเพิ่มฝ่ายบริการลูกค้าลงในแอปเนทีฟ คือโหลดหน้าวิดเจ็ตแบบเต็มหน้าจอที่เราโฮสต์ไว้ใน WebView คุณจะได้วิดเจ็ตเดียวกับบนเว็บไซต์ทุกประการ ทั้งหน้าแรก ข้อความ ศูนย์ช่วยเหลือ การค้นหา อีโมจิ ไฟล์แนบ เวลาของข้อความ สถานะอ่านแล้ว และการปิดบทสนทนา ทั้งยังอัปเดตอัตโนมัติ เมื่อเชื่อมต่อแล้ว แอปไม่ต้องออกเวอร์ชันใหม่อีกเลยเมื่อวิดเจ็ตมีการเปลี่ยนแปลง
เริ่มต้นอย่างรวดเร็ว
ให้ WebView โหลด URL นี้ สำหรับผู้ใช้ที่ล็อกอินอยู่ ต้องใส่ externalId และ email (และควรใส่ name ด้วย) ไม่เช่นนั้นเจ้าหน้าที่จะเห็นเป็นผู้เยี่ยมชมที่ไม่ระบุชื่อเสมอ ทุกค่าต้องเข้ารหัส URL (URL-encode):
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⚠️ เจ้าหน้าที่เห็น “ผู้เยี่ยมชมที่ไม่ระบุชื่อ” โดยไม่มีอีเมล? นี่คือข้อผิดพลาดในการเชื่อมต่อที่พบบ่อยที่สุด: URL ของ WebView ไม่มี email/externalId เซิร์ฟเวอร์สร้างค่าเหล่านี้ขึ้นเองไม่ได้ แอปต้องส่งมาเอง หาก ID ของผู้เยี่ยมชมมีหน้าตาแบบ anon-… แปลว่าไม่มีการส่งอะไรมาเลย
JavaScript / WebView ใดก็ได้ (ไม่ใช่ Flutter)
สำหรับ Electron, เชลล์เดสก์ท็อป, React Native หรือ WKWebView แบบเนทีฟที่คุณกำหนด URL เอง ให้สร้าง URL ด้วย JS โดย 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 URLFlutter (webview_flutter)
เพิ่ม webview_flutter: ^4.x ลงใน pubspec.yaml จากนั้น:
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()));สำหรับ WebView บน iOS/Android ในสแต็กอื่น (Swift WKWebView, Kotlin WebView, React Native react-native-webview) หลักการเหมือนกันทุกประการ คือโหลด URL และเปิดใช้ JavaScript
iOS และ Android แบบเนทีฟ
ไม่ต้องติดตั้ง SDK ใด ๆ แค่สร้าง URL แบบเดียวกันแล้วส่งให้ WebView โปรดสังเกตกรณีของ Mac: บิลด์สำหรับ App Store และบิลด์ DMG ทำงานบนระบบปฏิบัติการเดียวกัน ค่านี้จึงต้องมาจากแฟล็กตอนบิลด์ ไม่ใช่จากการตรวจสอบตอนรันไทม์
// ── 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คือภาษาของอินเทอร์เฟซ แทบทุกกรณีไม่ต้องส่งค่านี้ เพราะวิดเจ็ตจะใช้ภาษาของอุปกรณ์ตามที่ WebView รายงาน ซึ่งตรงกับที่ผู้ใช้คาดหวัง อินเทอร์เฟซของวิดเจ็ตแปลไว้ครบทั้ง 50 ภาษาที่ App Store Connect รองรับ ส่วนภาษาที่อยู่นอกรายการจะใช้ภาษาอังกฤษแทน ให้ส่งค่านี้เฉพาะเมื่อแอปของคุณมีตัวสลับภาษาในแอปเอง โดยส่งภาษาที่ผู้ใช้เลือกพร้อมforceLocale=1เพื่อให้มีผลเหนือการตั้งค่าของอุปกรณ์ ห้ามกำหนดค่าตายตัวเด็ดขาด การฮาร์ดโค้ดzh-Hansคือสาเหตุที่แอปภาษาญี่ปุ่นอาจแสดงวิดเจ็ตเป็นภาษาจีนplatform(แนะนำอย่างยิ่ง) ระบุว่าผู้เยี่ยมชมใช้บิลด์ใด ค่านี้กำหนดว่าผู้เยี่ยมชมจะเห็นบทความช่วยเหลือใดบ้าง และ AI จะตอบภายใต้กฎของ App Store หรือไม่ หากไม่ส่ง ระบบจะเดาแพลตฟอร์มจาก User-Agent ของ WebView ซึ่งแยกได้ว่าเป็น iPhone, Android หรือ Windows แต่แยกบิลด์ Mac App Store กับบิลด์ DMG ไม่ออก และไม่รู้จัก iPad ที่อยู่ในโหมดเดสก์ท็อป ค่าที่ใช้ได้:ios(iPhone และ iPad),macos_appstore(บิลด์ Mac App Store),macos(บิลด์ DMG ที่แจกจ่ายโดยตรง),android,windows,webexternalIdคือ ID ผู้ใช้ที่ไม่ซ้ำกันในแอปของคุณ การส่งค่านี้ช่วยให้เจ้าหน้าที่จำผู้ใช้ได้และรวมประวัติการสนทนาของผู้ใช้เข้าด้วยกันemail,name,avatarUrlคือข้อมูลโปรไฟล์ที่แสดงให้เจ้าหน้าที่เห็นhmacคือลายเซ็นยืนยันตัวตน (ดูด้านล่าง) ไม่บังคับ
ซ่อนเนื้อหาตามแพลตฟอร์ม (การปฏิบัติตามกฎ App Store)
กฎข้อ 3.1.1 ของ App Store ไม่อนุญาตให้แอป iOS แสดงเนื้อหาเกี่ยวกับการซื้อหรือการสมัครสมาชิกนอกแอป รวมถึงการแนะนำ (referral) ดังนั้นบทความช่วยเหลือและคำถามที่พบบ่อยทุกรายการจึงมีการตั้งค่าการแสดงผลแยกตามแพลตฟอร์ม เพียงเปิดหน้าศูนย์ช่วยเหลือหรือคำถามที่พบบ่อยในแดชบอร์ด แล้วสลับชิปแพลตฟอร์มของรายการนั้น ผู้เยี่ยมชมบนแพลตฟอร์มที่เลือกจะไม่เห็นรายการนั้นในลิสต์ และเปิดผ่านลิงก์ตรงก็ไม่ได้
AI ไม่ได้ตอบจากบทความช่วยเหลือหรือคำถามที่พบบ่อย แต่ตอบจากสิ่งที่คุณเพิ่มไว้ใน “Training” ไฟล์และคู่คำถาม-คำตอบในนั้นมีชิปแพลตฟอร์มแบบเดียวกัน รายการที่ซ่อนจากแพลตฟอร์มใดจะไม่ถูกใช้ตอบผู้เยี่ยมชมบนแพลตฟอร์มนั้น หากไม่ต้องการให้ AI พูดถึงด้วย ให้ซ่อนทั้งสองที่
มีสองเรื่องที่ตัดสินว่าฟีเจอร์นี้จะทำงานได้อย่างแน่นอนหรือไม่ อย่างแรก แอปของคุณควรส่ง platform มาเอง การเดาจาก User-Agent เป็นแค่ทางสำรอง อย่างที่สอง ค่านั้นต้องสะท้อนบิลด์ ไม่ใช่แค่ระบบปฏิบัติการ:
- iPad ไม่ได้แยกต่างหาก iPad รันแอป iOS ตัวเดียวกันภายใต้กฎชุดเดียวกัน ดังนั้น
iosจึงครอบคลุมทั้ง iPhone และ iPad - Mac มีสองบิลด์ บิลด์ Mac App Store ต้องปฏิบัติตามกฎข้อ 3.1.1 แต่ไฟล์ DMG ที่คุณแจกจ่ายจากเว็บไซต์ของตัวเองไม่ต้อง ทั้งสองทำงานบนระบบปฏิบัติการเดียวกัน จึงไม่มีสิ่งใดตอนรันไทม์ที่แยกทั้งสองออกจากกันได้ ให้ตัดสินตอนบิลด์ (เช่น ด้วยแฟล็กของ Xcode) แล้วส่ง
macos_appstoreหรือmacosให้ตรงกัน หากตั้งผิด บิลด์สำหรับสโตร์อาจแสดงเนื้อหาการสมัครสมาชิกระหว่างการรีวิว หรือบิลด์ DMG อาจซ่อนศูนย์ช่วยเหลือไปครึ่งหนึ่งโดยไม่มีเหตุผล
การเปลี่ยนแปลงมีผลทันที เพราะการตั้งค่านี้อยู่ในแดชบอร์ด ไม่ได้อยู่ในแอป คุณจึงไม่ต้องออกเวอร์ชันใหม่เพื่อเปลี่ยนสิ่งที่แสดง
การยืนยันตัวตน (HMAC, ไม่บังคับ)
หากต้องการพิสูจน์ว่า externalId เป็นผู้ใช้ที่ล็อกอินอยู่ของคุณจริง (และป้องกันการสวมรอย) ให้ส่งลายเซ็น HMAC มาด้วย โดยคำนวณบนเซิร์ฟเวอร์ของคุณ ห้ามใส่คีย์ลับไว้ในแอปเด็ดขาด
hmac = HMAC-SHA256(secretKey, externalId)โดยแสดงผลเป็นเลขฐานสิบหกตัวพิมพ์เล็ก- ข้อมูลที่ใช้ลงนาม (payload) คือ
externalIdจึงต้องส่งมาด้วยทุกครั้ง หากไม่ส่ง วิดเจ็ตจะใช้ ID นิรนามของตัวเอง และลายเซ็นที่ลงนามจากemailอย่างเดียวจะถูกปฏิเสธ secretKeyคือคีย์ลับของกล่องข้อความ (หนึ่งคีย์ต่อหนึ่งกล่องข้อความ ดูได้ในการตั้งค่ากล่องข้อความ) หากไม่ส่ง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จะเปิดตัวเลือกรูปภาพแบบเนทีฟเอง คุณจึงไม่ต้องสร้างอะไรเพิ่ม การเลือกรูปจากคลังรูปภาพไม่ต้องขอสิทธิ์ใด ๆ ส่วนNSCameraUsageDescriptionในInfo.plistจำเป็นเฉพาะเมื่อต้องการตัวเลือกกล้อง “ถ่ายรูป” หากไม่ใส่ แอปจะแครชเมื่อผู้ใช้แตะตัวเลือกนั้น
ต้องการ UI แบบเนทีฟ?
หากคุณต้องการหน้าจอแชทแบบเนทีฟที่สร้างเองแทน WebView ให้ใช้ไคลเอนต์ที่มีเฉพาะส่วนตรรกะในหน้า SDK สำหรับมือถือ แต่สำหรับแอปส่วนใหญ่ WebView ข้างต้นเปิดใช้งานได้เร็วกว่าและมีฟีเจอร์ครบเสมอ