الأدلة

الدردشة داخل التطبيق (WebView)

ضمِّن أداة الدردشة الكاملة في أي تطبيق للجوال أو سطح المكتب عبر WebView، بكل الميزات ومع تحديث تلقائي.

أسرع طريقة لإضافة دعم العملاء داخل تطبيق أصلي هي تحميل صفحة أداة الدردشة المستضافة لدينا بملء الشاشة داخل WebView. ستحصل على الأداة نفسها تمامًا الموجودة على الموقع: الرئيسية، والرسائل، ومركز المساعدة، والبحث، والرموز التعبيرية، والمرفقات، والطوابع الزمنية، وإشعارات القراءة، وإغلاق المحادثة، كما أنها تتحدّث تلقائيًا. بعد الدمج مرة واحدة لن يحتاج التطبيق أبدًا إلى إصدار جديد بسبب أي تغيير في الأداة.

البدء السريع

اجعل WebView يحمّل هذا العنوان. إذا كان المستخدم مسجّل الدخول فيجب تضمين 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 لديك يخلو من email/externalId. لا يستطيع الخادم اختراع هذه القيم، بل يجب أن يمرّرها التطبيق. وإذا كان معرّف الزائر يشبه anon-… فهذا يعني أنه لم يُمرَّر أي شيء.

JavaScript / أي WebView (غير Flutter)

في Electron، أو أغلفة تطبيقات سطح المكتب، أو React Native، أو WKWebView أصلي تضبط فيه العنوان بنفسك، ابنِ العنوان بلغة JavaScript. يتولى 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)

أضف 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) فالفكرة واحدة: حمِّل العنوان وفعِّل JavaScript.

تطبيقات iOS وAndroid الأصلية

لا توجد SDK لتثبيتها، فأنت تبني العنوان نفسه وتسلّمه إلى WebView. انتبه لحالة Mac: إصدار App Store وإصدار DMG يعملان على نظام التشغيل نفسه، لذا يجب أن تأتي القيمة من علامة (flag) وقت البناء، لا من فحص وقت التشغيل.

// ── 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، وهذا ما يتوقعه المستخدمون. واجهة الأداة مترجمة إلى جميع لغات App Store Connect الخمسين، وأي لغة خارج هذه القائمة تعود إلى الإنجليزية. مرّره فقط إذا كان لتطبيقك مبدّل لغة خاص به داخل التطبيق، وحينها أرسل اللغة التي اختارها المستخدم مع forceLocale=1 لتتقدّم على إعداد الجهاز. لا تكتب قيمة ثابتة أبدًا: فالقيمة الثابتة zh-Hans هي السبب في أن تطبيقًا يابانيًا قد ينتهي بعرض أداة دردشة صينية.
  • platform (موصى به بشدة): الإصدار الذي يستخدمه الزائر. يحدد مقالات المساعدة التي يراها، وما إذا كان الذكاء الاصطناعي يجيب وفق قواعد 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، وweb.
  • externalId: المعرّف الفريد للمستخدم في تطبيقك. تمريره يتيح لموظف الدعم التعرّف على المستخدم ودمج سجلّ محادثاته.
  • email، name، avatarUrl: بيانات الملف الشخصي التي تظهر لموظفي الدعم.
  • hmac: توقيع الهوية (انظر أدناه). اختياري.

إخفاء المحتوى حسب المنصة (الامتثال لقواعد App Store)

لا تسمح القاعدة 3.1.1 من قواعد App Store لتطبيق iOS بعرض محتوى عن الشراء أو الاشتراك خارج المتجر أو عن برامج الإحالة. لذلك تحمل كل مقالة مساعدة وكل سؤال شائع إعدادًا للظهور حسب المنصة: افتح صفحة مركز المساعدة أو الأسئلة الشائعة في لوحة التحكم، وبدّل شارات المنصات على ذلك العنصر. عندها لن يراه الزوار على المنصة المحددة في القائمة، ولن يتمكنوا من فتحه برابط مباشر.

لا يجيب الذكاء الاصطناعي من مقالات المساعدة أو الأسئلة الشائعة، بل مما أضفته في «Training». للملفات وأزواج الأسئلة والأجوبة هناك شارات المنصات نفسها، والعنصر المخفي عن منصة ما لا يُستخدم في الإجابة على زوار تلك المنصة. وإن كان يجب ألا يذكره الذكاء الاصطناعي أيضًا، فأخفِه في المكانين.

أمران يحددان ما إذا كان هذا يعمل بشكل موثوق. أولًا، يجب أن يرسل تطبيقك platform بنفسه؛ فالاستنتاج من User-Agent مجرد حل احتياطي. ثانيًا، يجب أن تعكس القيمة الإصدار (build)، لا نظام التشغيل فقط:

  • 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)، ويكون الناتج بالنظام الست عشري بأحرف صغيرة.
  • البيانات الموقَّعة هي externalId، لذا مرّره دائمًا: فمن دونه تستخدم الأداة معرّفًا مجهولًا خاصًا بها، ويُرفض أي توقيع على 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 إلا إذا أردت خيار الكاميرا «التقاط صورة»، ومن دونه يتعطل التطبيق عندما ينقر المستخدم هذا الخيار.

تفضّل واجهة أصلية؟

إذا كنت تحتاج إلى واجهة دردشة أصلية تبنيها بنفسك بدلًا من WebView، فاستخدم مكتبات العميل التي تتولى المنطق فقط (دون واجهة) في صفحة حِزم SDK للجوال. أما لمعظم التطبيقات، فإن WebView أعلاه أسرع في الإطلاق ويقدّم الميزات كاملة دائمًا.