إضافة الدردشة إلى تطبيق الجوال
تعمل دردشة وكيل مجيب أيضاً داخل تطبيقك على iOS أو Android. يفتح التطبيق صفحة الدردشة في WebView بمعرّف أداتك، فيتحدث الناس مع الوكيل نفسه الذي يجيب على موقعك. لا تحتاج إلى مفتاح API ولا إلى SDK أو مكتبة أصلية (native) من مجيب. ويتضمن هذا الدليل مكوّن React Native تنسخه.
كيف تعمل الدردشة داخل تطبيق؟
الدردشة صفحة ويب، https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile: هي الدردشة نفسها التي تفتحها أداة الموقع، فيستطيع أي WebView تحميلها. تجيب عبر وكيلك، وتُخبر تطبيقك متى تصبح جاهزة للعرض، ومتى ينقر الزائر ✕ فيها لإغلاقها.
يقرر تطبيقك متى يُظهر الدردشة ومتى يخفيها. وكل ما بداخلها يعمل كما على موقعك: النصوص والصور والملفات والرسائل الصوتية ومحادثات الزائر السابقة والردود المباشرة. وفي التطبيق تظهر الدردشة بمظهرها الافتراضي، رأس أسود بعنوان "Support team"، لا بلون أداتك.
أين أجد معرّف أداتي؟
في كود الأداة من لوحة التحكم، أو من الواجهة البرمجية العامة. انسخ الكود من الموقع الإلكتروني في صفحة التوصيل، كما تفعل لموقع ويب (راجع تضمين في موقعك)، واستخدم قيمة data-widget-id فيه. يحتاج تطبيقك إلى هذا المعرّف وحده، لا إلى الكود نفسه.
عبر الواجهة البرمجية، يُعيد GET /v1/agents/{agentId}/widget الأداة نفسها، وid فيها هو معرّف الأداة؛ راجع أداة الموقع ومرجع جلب أداة الموقع لوكيل. أجرِ هذا الاستدعاء من خادمك أو من سكربت، لا من التطبيق أبداً: فهو يحتاج إلى مفتاح API.
معرّف الأداة ليس سرّياً. فهو في مصدر صفحة كل موقع توجد فيه أداتك، ولا يتيح إلا الدردشة مع وكيلك.
ما العنوان الذي يحمّله التطبيق؟
https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile، مع معرّف أداتك مكان YOUR_WIDGET_ID. أبقِ deviceType=mobile لتعمل لوحة مفاتيح الدردشة كما على الهاتف. أضف initialMessage لتفتح الدردشة بسطر من وكيلك، وشفّره بـencodeURIComponent.
| المُعامل | مطلوب | ما يفعله |
|---|---|---|
widgetId | نعم | معرّف أداتك. تذهب المحادثات إلى وكيلها. |
deviceType | نعم، بالقيمة mobile | يضيف مفتاح الإدخال (Return) سطراً جديداً ويُرسل زر الإرسال الرسالة، كما على الهاتف. ودونه يُرسل مفتاح الإدخال الرسالة. |
initialMessage | لا | نص مشفّر للعنوان (URL-encoded) تعرضه الدردشة سطراً افتتاحياً من الوكيل، في نافذة الدردشة هذه فقط. |
مثال: https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile&initialMessage=Need%20help%20with%20your%20order%3F
كيف أضيف الدردشة إلى تطبيق React Native؟
ثبّت react-native-webview وreact-native-safe-area-context، وانسخ مكوّن MojeebChat أدناه إلى تطبيقك، واعرضه مرة واحدة بمعرّف أداتك. اجعل open يساوي true لتُظهر الدردشة. ويستدعي المكوّن onClose حين ينقر الزائر ✕ في الدردشة أو زر الرجوع في Android، فيعيد تطبيقك open إلى false.
npx expo install react-native-webview react-native-safe-area-context
دون Expo، ثبّت الحزمتين بمدير الحزم لديك، ثم شغّل pod install في مجلد ios.
import { useEffect, useRef, useState } from 'react';
import { BackHandler, KeyboardAvoidingView, Linking, Platform, Pressable, StyleSheet, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
const CHAT_URL = 'https://chat.mojeeb.app/';
// The chat posts to window.parent, which in a WebView is the page itself: forward to React Native.
const FORWARD_MESSAGES = `
window.addEventListener('message', function (e) {
var type = e.data && e.data.type;
if (type === 'widgetReady' || type === 'closeWidget') window.ReactNativeWebView.postMessage(type);
});
true;
`;
type Props = { widgetId: string; open: boolean; onClose: () => void; message?: string };
export function MojeebChat({ widgetId, open, onClose, message }: Props) {
const webView = useRef<WebView>(null);
const [url, setUrl] = useState('');
const [ready, setReady] = useState(false);
// Load on first open; after that, reload only to show a different opening message.
useEffect(() => {
if (!open) return;
let next = `${CHAT_URL}?widgetId=${encodeURIComponent(widgetId)}&deviceType=mobile`;
if (message) next += `&initialMessage=${encodeURIComponent(message)}`;
if (!url || (message && next !== url)) setUrl(next);
}, [open, widgetId, message]);
// Tell the chat whether it can be seen: it goes quiet while hidden and catches up when shown.
useEffect(() => {
if (!ready) return;
const type = open ? 'widgetShown' : 'widgetHidden';
webView.current?.injectJavaScript(`window.postMessage({ type: '${type}' }, '*'); true;`);
}, [open, ready]);
// Android's back button closes the chat.
useEffect(() => {
if (!open) return;
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
onClose();
return true;
});
return () => sub.remove();
}, [open, onClose]);
return (
// Hidden with opacity, not display: 'none', which would drop the WebView and reload the chat.
<SafeAreaView
style={[StyleSheet.absoluteFill, styles.overlay, !open && styles.hidden]}
pointerEvents={open ? 'auto' : 'none'}
accessibilityElementsHidden={!open}
importantForAccessibility={open ? 'auto' : 'no-hide-descendants'}
>
{/* iOS: shrink the chat above the keyboard, or the page scrolls its header off screen. */}
<KeyboardAvoidingView style={styles.fill} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
{!!url && (
<WebView
ref={webView}
source={{ uri: url }}
style={[styles.fill, !ready && styles.hidden]}
injectedJavaScriptBeforeContentLoaded={FORWARD_MESSAGES}
onMessage={(e) => {
if (e.nativeEvent.data === 'widgetReady') setReady(true);
if (e.nativeEvent.data === 'closeWidget') onClose();
}}
onLoadStart={() => setReady(false)}
onContentProcessDidTerminate={() => webView.current?.reload()}
mediaCapturePermissionGrantType="grant"
allowsInlineMediaPlayback
scrollEnabled={false}
bounces={false}
hideKeyboardAccessoryView
onOpenWindow={(e) => Linking.openURL(e.nativeEvent.targetUrl)}
onShouldStartLoadWithRequest={(req) => {
if (!req.isTopFrame || !/^https?:/i.test(req.url) || req.url.startsWith(CHAT_URL)) return true;
Linking.openURL(req.url);
return false;
}}
/>
)}
{!ready && (
<Pressable onPress={onClose} style={styles.close} accessibilityRole="button" accessibilityLabel="Close chat">
<Text style={styles.closeText}>✕</Text>
</Pressable>
)}
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
overlay: { backgroundColor: '#fff', zIndex: 10 },
hidden: { opacity: 0 },
fill: { flex: 1 },
close: { position: 'absolute', top: 8, right: 8, padding: 12 },
closeText: { fontSize: 20, color: '#555' },
});
ثم اعرضه بعد محتوى تطبيقك، ليغطي الشاشة حين يُفتح:
import { useCallback, useState } from 'react';
import { Button, View } from 'react-native';
import { MojeebChat } from './MojeebChat';
export default function App() {
const [chatOpen, setChatOpen] = useState(false);
const closeChat = useCallback(() => setChatOpen(false), []);
return (
<View style={{ flex: 1 }}>
{/* ...your app... */}
<Button title="Chat with us" onPress={() => setChatOpen(true)} />
<MojeebChat widgetId="YOUR_WIDGET_ID" open={chatOpen} onClose={closeChat} />
</View>
);
}
ضع MojeebChat في مكان يبقى فيه مركَّباً (mounted)، كالتخطيط الجذري لتطبيقك: فإزالته تتخلص من الدردشة، وتحمّلها الفتحة التالية من البداية. وإلى أن تجهز الدردشة، يعرض المكوّن خلفية بيضاء بسيطة مع ✕ خاص به، فيستطيع الزائر إغلاقها دائماً. الكود مكتوب بـTypeScript؛ ولـJavaScript احذف النوع Props و: Props و<WebView>.
كيف أفتح الدردشة برسالة افتتاحية؟
مرّر message إلى مكوّن MojeebChat، فيضيفها إلى العنوان باسم initialMessage. تعرضها الدردشة سطراً أول من وكيلك، مثل "هل تحتاج مساعدة في الطلب رقم 1042؟" حين تُفتح من شاشة طلب. ولا تظهر إلا في نافذة الدردشة هذه: فهي لا تُحفظ في المحادثة، فلا يراها فريقك ولا ذكاء الوكيل الاصطناعي.
وهي تعمل مثل MojeebWidget.open({ message }) في موقع ويب. ولأن الذكاء الاصطناعي لا يرى السطر الافتتاحي، فاكتبه تحيةً لا معلومةً يحتاجها الوكيل. ولا يعيد MojeebChat تحميل الدردشة إلا حين تتغيّر الرسالة؛ وفتحها مجدداً دون رسالة يُبقي الدردشة كما كانت.
ما الرسائل التي ترسلها الدردشة إلى تطبيقي؟
رسالتان، كل منهما كائن فيه type: widgetReady بعد أن ترسم الدردشة واجهتها، وcloseWidget حين ينقر الزائر ✕ في الدردشة. أظهر الدردشة عند الأولى وأخفها عند الثانية. تُرسلهما الصفحة عبر window.parent.postMessage، الذي يصل في WebView إلى الصفحة نفسها، لذا على تطبيقك أن يمرّرهما.
type | متى تُرسَل | ما يفعله تطبيقك |
|---|---|---|
widgetReady | بعد أن ترسم الدردشة واجهتها، بعد كل تحميل | يُظهر WebView |
closeWidget | نقر الزائر ✕ في الدردشة | يُخفي الدردشة |
يحمّل WebView الدردشة صفحةً رئيسية (top-level)، فيكون window.parent هو نافذة الدردشة نفسها. احقن قبل تحميل الصفحة مستمعاً يمرّر الرسالتين إلى الكود الأصلي (native). في React Native يتم ذلك بـinjectedJavaScriptBeforeContentLoaded، الذي يمرّرهما عبر window.ReactNativeWebView.postMessage إلى onMessage في WebView، كما يفعل FORWARD_MESSAGES في MojeebChat.
ما الرسائل التي يستطيع تطبيقي إرسالها إلى الدردشة؟
رسالتان اختياريتان، للتطبيقات التي تُبقي الدردشة محمّلة وهي مخفية: widgetHidden حين تخفيها، وwidgetShown حين تُظهرها مجدداً. وما دامت مخفية تهدأ الدردشة؛ وحين تظهر تستدرك كل ما فاتها. أرسلهما بتشغيل هذا في صفحة الدردشة، كما يفعل MojeebChat بـinjectJavaScript:
window.postMessage({ type: 'widgetHidden' }, '*'); // when you hide the chat
window.postMessage({ type: 'widgetShown' }, '*'); // when you show it again
والتطبيق الذي يُتلف WebView عند إغلاق الدردشة لا يحتاج إلى أيٍّ منهما: فهو يحمّل دردشة جديدة في كل مرة.
لماذا أخفي الدردشة بدل إغلاقها؟
لتبقى جاهزة. الدردشة المخفية تبقى محمّلة، فتكون الفتحة التالية فورية وتُظهر المحادثة حيث تركها الزائر. يخفيها MojeebChat بـopacity: 0 وpointerEvents="none". وفي React Native لا تستخدم display: 'none': فالبنية الجديدة (new architecture) تتخلص من العروض الأصلية (native views) لأي شجرة فرعية بـdisplay: 'none'، فيُعاد تحميل الدردشة في كل فتحة.
ويُخفي MojeebChat الدردشة أيضاً عن قارئات الشاشة وهي مغلقة، ويُرسل widgetHidden وwidgetShown لتعرف الدردشة هل يراها الزائر.
كيف تبقى الدردشة على الشاشة حين تُفتح لوحة المفاتيح؟
في iOS، ضع WebView داخل KeyboardAvoidingView بـbehavior="padding"، كما يفعل MojeebChat، فتتقلص الدردشة لتتسع فوق لوحة المفاتيح. ودونه يمرّر iOS الصفحة كلها إلى الأعلى أثناء الكتابة، فيخرج رأس الدردشة، بعنوانه و✕، من الشاشة. وأوقف أيضاً التمرير الخاص بـWebView.
وفي MojeebChat يضبط WebView scrollEnabled={false} وbounces={false}، لأن الدردشة تمرّر قائمة رسائلها بنفسها، وhideKeyboardAccessoryView الذي يزيل الشريط الذي يضيفه iOS فوق لوحة المفاتيح.
كيف تُفتح الروابط والمرفقات؟
في متصفح الهاتف. الروابط في ردود الوكيل والمرفقات تُفتح في نافذة جديدة (target="_blank")، وهو ما يتجاهله WebView ما لم يتعامل معه تطبيقك. في MojeebChat يمرّرها onOpenWindow إلى Linking.openURL، ويرسل onShouldStartLoadWithRequest إلى المتصفح أيضاً أي صفحة أخرى قد تنتقل إليها الدردشة، فلا تغادر الدردشة WebView أبداً.
ويسمح onShouldStartLoadWithRequest بكل تحميل ضمن عنوان الدردشة، وبالتحميل في الإطارات غير الإطار الرئيسي، وبالعناوين التي ليست http أو https.
ماذا يحدث حين يُعاد تحميل الدردشة؟
يُخفي MojeebChat الـWebView من بداية كل تحميل إلى أن ترسل الدردشة widgetReady، فيرى الزائر خلفية بسيطة مع ✕ بدل صفحة نصف محمّلة. وقد يُنهي iOS عملية WebView مخفي لتحرير الذاكرة؛ فيعيد onContentProcessDidTerminate عندها تحميل الدردشة، وتعود معها محادثة الزائر.
ما الأذونات التي يحتاجها تطبيقي؟
الميكروفون للرسائل الصوتية، والكاميرا ومكتبة الصور للصور. صرّح بها كما تفعل لأي ميزة أخرى في تطبيقك؛ ويسأل النظام الزائر في أول مرة تحتاج فيها الدردشة إلى إحداها. وفي iOS، يمنع mediaCapturePermissionGrantType="grant" (من iOS 15 فما بعده) محرك WebKit من طلب الميكروفون مرة ثانية بعد أن طلبه النظام.
| المنصة | صرّح بـ | من أجل |
|---|---|---|
iOS (Info.plist) | NSMicrophoneUsageDescription | الرسائل الصوتية |
iOS (Info.plist) | NSCameraUsageDescription | التقاط صورة |
iOS (Info.plist) | NSPhotoLibraryUsageDescription | اختيار صورة |
Android (AndroidManifest.xml) | android.permission.RECORD_AUDIO | الرسائل الصوتية |
Android (AndroidManifest.xml) | android.permission.CAMERA | التقاط صورة |
مع Expo، اضبطها في app.json: نصوص iOS تحت ios.infoPlist، وأذونات Android تحت android.permissions:
{
"expo": {
"ios": {
"infoPlist": {
"NSMicrophoneUsageDescription": "Record voice notes in the support chat.",
"NSCameraUsageDescription": "Take photos to send in the support chat.",
"NSPhotoLibraryUsageDescription": "Choose photos to send in the support chat."
}
},
"android": {
"permissions": ["android.permission.RECORD_AUDIO", "android.permission.CAMERA"]
}
}
}
يتضمن Expo Go حزمة react-native-webview، فتستطيع تجربة الدردشة فيه، لكن لـExpo Go أذوناته الخاصة. أما أذوناتك فتسري في نسخة تطوير (development build) أو نسخة المتجر من تطبيقك، تبنيها بعد تغييرها.
أسئلة شائعة
هل أستطيع تمرير اسم مستخدم التطبيق أو بريده أو معرّفه إلى الدردشة؟
لا. تبدأ كل دردشة من تطبيق بزائر ضيف مجهول، كما في الموقع، ولا تأخذ صفحة الدردشة أي بيانات عن المستخدم. وإن شارك الزائر اسمه أو هاتفه أو بريده في المحادثة، يستطيع الوكيل حفظه عميلاً محتملاً، كما يفعل على موقعك.
هل تبقى المحادثة بعد إعادة تشغيل التطبيق؟
نعم. تحفظ الدردشة هوية الزائر الضيف في تخزين WebView، فتستمر المحادثة نفسها بعد إعادة التشغيل. ولا يعيد تسجيل الخروج من تطبيقك ضبطها، لأن الدردشة لا تعرف شيئاً عن حسابات تطبيقك: فعلى هاتف مشترك يرى الشخص التالي الدردشة نفسها. أما مسح البيانات المخزّنة في WebView فيبدأ ضيفاً جديداً.
هل يصل إلى تطبيقي إشعار حين يرد الوكيل؟
لا. لا ترسل الدردشة إشعارات فورية (push). وما دامت الدردشة مفتوحة تظهر ردود الوكيل وفريقك مباشرة. وإن كان الزائر قد غادر التطبيق، فالرد ينتظره في المحادثة في المرة التالية التي يفتح فيها الدردشة.
أين تظهر دردشات التطبيق في لوحة التحكم؟
في المحادثات، مع محادثات أداة موقعك، تحت فلتر المنصة الموقع. يجيب عنها الوكيل نفسه بالمعرفة نفسها، ويستطيع فريقك الرد وتولّي المحادثة كما في أي دردشة على الموقع، ويظهر كل زائر أيضاً في صفحة جهات الاتصال.
هل أحتاج إلى مفتاح API؟
لا. يحتاج التطبيق إلى معرّف الأداة وحده، وهو ليس سرّياً: فهو في مصدر صفحة كل موقع فيه أداتك، ولا يتيح إلا الدردشة مع وكيلك. لا تضع مفتاح API من مجيب في تطبيق أبداً. فأي أحد يستطيع استخراجه من التطبيق، والمفتاح يستطيع تغيير وكلائك وقراءة جهات اتصالك.
ما الباقة التي أحتاجها؟
أي باقة. يستخدم التطبيق أداة موقعك، وهي مشمولة في كل الباقات، ولا يحتاج إلى مفتاح API، فلا تلزمه الواجهة البرمجية العامة التي تتطلب باقة مدفوعة. وتُحسب ردود الوكيل في دردشات التطبيق من الرسائل الشهرية لباقتك، كما تُحسب ردوده على موقعك. راجع الخطط والأسعار.
هل أستطيع تغيير لون الدردشة من التطبيق؟
لا. في التطبيق تظهر الدردشة بمظهرها الافتراضي، رأس أسود بعنوان "Support team"، أياً كان اللون الأساسي الذي تضبطه في لوحة التحكم. فهذا الإعداد خاص بزر الدردشة والدردشة على موقعك.
هل تظهر الرسالة الأولية من لوحة التحكم في التطبيق؟
لا. الرسالة الأولية التي تضبطها في لوحة التحكم خاصة بالفقاعة بجانب زر الدردشة في موقعك، وليس في التطبيق زر دردشة. ولتحيّي الناس في تطبيقك، مرّر سطرك الافتتاحي في المُعامل initialMessage، الذي تضبطه الخاصية message في MojeebChat. ويظهر في نافذة الدردشة تلك فقط.
هل تعمل مع Flutter، أو في تطبيقات iOS وAndroid الأصلية؟
نعم. الدردشة صفحة ويب، فيستطيع أي WebView عرضها؛ ويعرض هذا الدليل React Native. وفي أي إطار عمل افعل الأمور نفسها: حمّل عنوان الدردشة، ومرّر widgetReady وcloseWidget إلى كودك، وافتح الروابط في المتصفح، وصرّح بأذونات الميكروفون والكاميرا، وأخفِ WebView بدل إتلافه.