# إضافة الدردشة إلى تطبيق الجوال

> اعرض دردشة وكيل مجيب داخل تطبيقك على iOS أو Android بتحميل https://chat.mojeeb.app بمعرّف أداتك في WebView، مع مكوّن React Native تنسخه، ورسالة افتتاحية اختيارية، والأذونات التي تصرّح بها، والرسائل التي تتبادلها الدردشة مع تطبيقك.

تعمل دردشة وكيل مجيب أيضاً داخل تطبيقك على iOS أو Android. يفتح التطبيق صفحة الدردشة في WebView بمعرّف أداتك، فيتحدث الناس مع الوكيل نفسه الذي يجيب على موقعك. لا تحتاج إلى مفتاح API ولا إلى SDK أو مكتبة أصلية (native) من مجيب. ويتضمن هذا الدليل مكوّن React Native تنسخه.

## كيف تعمل الدردشة داخل تطبيق؟

الدردشة صفحة ويب، `https://chat.mojeeb.app/?widgetId=YOUR_WIDGET_ID&deviceType=mobile`: هي الدردشة نفسها التي تفتحها [أداة الموقع](/docs/channels/website-widget)، فيستطيع أي WebView تحميلها. تجيب عبر وكيلك، وتُخبر تطبيقك متى تصبح جاهزة للعرض، ومتى ينقر الزائر ✕ فيها لإغلاقها.

يقرر تطبيقك متى يُظهر الدردشة ومتى يخفيها. وكل ما بداخلها يعمل كما على موقعك: النصوص والصور والملفات والرسائل الصوتية ومحادثات الزائر السابقة والردود المباشرة. وفي التطبيق تظهر الدردشة بمظهرها الافتراضي، رأس أسود بعنوان "Support team"، لا بلون أداتك.

## أين أجد معرّف أداتي؟

في كود الأداة من لوحة التحكم، أو من الواجهة البرمجية العامة. انسخ الكود من **الموقع الإلكتروني** في صفحة [التوصيل](https://dashboard.mojeeb.app/connections)، كما تفعل لموقع ويب (راجع [تضمين في موقعك](/docs/getting-started/embed-on-website))، واستخدم قيمة `data-widget-id` فيه. يحتاج تطبيقك إلى هذا المعرّف وحده، لا إلى الكود نفسه.

عبر الواجهة البرمجية، يُعيد `GET /v1/agents/{agentId}/widget` الأداة نفسها، و`id` فيها هو معرّف الأداة؛ راجع [أداة الموقع](./agents/website-widget) و[مرجع جلب أداة الموقع لوكيل](./api-reference/get-widget). أجرِ هذا الاستدعاء من خادمك أو من سكربت، لا من التطبيق أبداً: فهو يحتاج إلى مفتاح 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`.

```bash
npx expo install react-native-webview react-native-safe-area-context
```

دون Expo، ثبّت الحزمتين بمدير الحزم لديك، ثم شغّل `pod install` في مجلد `ios`.

```tsx title="MojeebChat.tsx"

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' },
});
```

ثم اعرضه بعد محتوى تطبيقك، ليغطي الشاشة حين يُفتح:

```tsx

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`:

```js
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`:

```json title="app.json"
{
  "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) أو نسخة المتجر من تطبيقك، تبنيها بعد تغييرها.

## أسئلة شائعة

### هل أستطيع تمرير اسم مستخدم التطبيق أو بريده أو معرّفه إلى الدردشة؟

لا. تبدأ كل دردشة من تطبيق بزائر ضيف مجهول، كما في الموقع، ولا تأخذ صفحة الدردشة أي بيانات عن المستخدم. وإن شارك الزائر اسمه أو هاتفه أو بريده في المحادثة، يستطيع الوكيل حفظه [عميلاً محتملاً](/docs/features/lead-management)، كما يفعل على موقعك.

### هل تبقى المحادثة بعد إعادة تشغيل التطبيق؟

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

### هل يصل إلى تطبيقي إشعار حين يرد الوكيل؟

لا. لا ترسل الدردشة إشعارات فورية (push). وما دامت الدردشة مفتوحة تظهر ردود الوكيل وفريقك مباشرة. وإن كان الزائر قد غادر التطبيق، فالرد ينتظره في المحادثة في المرة التالية التي يفتح فيها الدردشة.

### أين تظهر دردشات التطبيق في لوحة التحكم؟

في [المحادثات](https://dashboard.mojeeb.app/conversations)، مع محادثات أداة موقعك، تحت فلتر المنصة **الموقع**. يجيب عنها الوكيل نفسه بالمعرفة نفسها، ويستطيع فريقك الرد وتولّي المحادثة كما في أي دردشة على الموقع، ويظهر كل زائر أيضاً في صفحة [جهات الاتصال](/docs/features/contacts).

### هل أحتاج إلى مفتاح API؟

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

### ما الباقة التي أحتاجها؟

أي باقة. يستخدم التطبيق أداة موقعك، وهي مشمولة في كل الباقات، ولا يحتاج إلى مفتاح API، فلا تلزمه [الواجهة البرمجية العامة](./overview) التي تتطلب باقة مدفوعة. وتُحسب ردود الوكيل في دردشات التطبيق من الرسائل الشهرية لباقتك، كما تُحسب ردوده على موقعك. راجع [الخطط والأسعار](/docs/billing/plans-and-pricing).

### هل أستطيع تغيير لون الدردشة من التطبيق؟

لا. في التطبيق تظهر الدردشة بمظهرها الافتراضي، رأس أسود بعنوان "Support team"، أياً كان **اللون الأساسي** الذي تضبطه في لوحة التحكم. فهذا الإعداد خاص بزر الدردشة والدردشة على موقعك.

### هل تظهر الرسالة الأولية من لوحة التحكم في التطبيق؟

لا. **الرسالة الأولية** التي تضبطها في لوحة التحكم خاصة بالفقاعة بجانب زر الدردشة في موقعك، وليس في التطبيق زر دردشة. ولتحيّي الناس في تطبيقك، مرّر سطرك الافتتاحي في المُعامل `initialMessage`، الذي تضبطه الخاصية `message` في `MojeebChat`. ويظهر في نافذة الدردشة تلك فقط.

### هل تعمل مع Flutter، أو في تطبيقات iOS وAndroid الأصلية؟

نعم. الدردشة صفحة ويب، فيستطيع أي WebView عرضها؛ ويعرض هذا الدليل React Native. وفي أي إطار عمل افعل الأمور نفسها: حمّل عنوان الدردشة، ومرّر `widgetReady` و`closeWidget` إلى كودك، وافتح الروابط في المتصفح، وصرّح بأذونات الميكروفون والكاميرا، وأخفِ WebView بدل إتلافه.
