دمج React Native SDK
غلاف React Native يربط SDK الأصلي لـ iOS وAndroid، ويمنحك إمكانات المنصتين كاملة من خلال API واحدة في TypeScript.
تتطلب الحزمة الأصلية iOS 14.0، بينما لا يزال React Native 0.75 يستخدم 13.4 افتراضيًا. يرفض CocoaPods هذا التعارض برسالة خطأ مضلِّلة - "Specs satisfying the dependency were found, but they required a higher minimum deployment target" - تبدو كأنها مشكلة في حل الإصدارات وليست مشكلة منصة.
ارفع الحد الأدنى في ios/Podfile:
platform :ios, [min_ios_version_supported, '14.0'].max
أخذ القيمة الأعلى من الاثنتين يبقى صالحًا عندما يرفع إصدار مستقبلي من React Native حدّه الأدنى فوق حدّنا.
ثم ارفع أيضًا هدف النشر (deployment target) للتطبيق نفسه إلى 14.0 أو أعلى: في Xcode اختر الـ target الخاص بالتطبيق، ثم General → Minimum Deployments → iOS (إعداد البناء IPHONEOS_DEPLOYMENT_TARGET في ios/<YourApp>.xcodeproj). يبقى تطبيق React Native 0.75 الجديد على 13.4 هناك حتى بعد تعديل Podfile، فيُصدر الـ linker تحذيرًا بأن ملفات Joryio بُنيت لإصدار iOS أحدث من إصدار التطبيق.
الخصائص
- Native Bridge: يغلف SDK الأصلي لـ iOS، Swift، وAndroid، Kotlin.
- تتبع الأحداث: تتبع الأحداث المخصصة ومشاهدات الشاشات.
- هوية المستخدم: تعريف المستخدمين وإضافة معرّفات بديلة (aliases) بتسمية وإدارة السمات.
- إشعارات Push: تسجيل الرموز وتتبع النقرات، FCM وAPNs.
- رسائل In-app: استقبال رسائل داخل التطبيق وعرضها.
- E-Commerce: تتبع المشتريات وأحداث السلة وإتمام الشراء.
- دعم العمل دون اتصال: توضع الأحداث في طابور محليًا وتُزامن عند الاتصال.
- إعادة المحاولة التلقائية: تراجع أسي عند فشل الشبكة.
المتطلبات
- React Native 0.75+. على iOS يُربط الـ SDK الأصلي كحزمة Swift عبر
spm_dependencyفي React Native، الذي أُضيف في 0.75. مع Expo، الإصدار SDK 52 أو أحدث. - البنية القديمة والبنية الجديدة. في البنية الجديدة تعمل الوحدة عبر طبقة التوافق (interop layer) في React Native للوحدات القديمة، وهي مفعّلة افتراضيًا.
- iOS 14.0+ / Android API 23+ (Android 6.0).
- TypeScript 5.0+، موصى به.
التثبيت
npm install @joryio/react-native-sdk
إعداد iOS
cd ios && pod install
يضيف pod install الـ Joryio iOS SDK إلى مشروع Pods كحزمة Swift
(https://github.com/joryio/joryio-ios-sdk، المنتجان Joryio وJoryioUI، بالإصدار
نفسه لهذه الحزمة تماماً)، ويُنزّلها Xcode في أول مرة يبني فيها التطبيق - لذا يحتاج
ذلك البناء إلى اتصال شبكي بـ github.com. لا يأتي شيء من CocoaPods trunk، الذي
يتوقف عن قبول إصدارات جديدة في 2 ديسمبر 2026.
يطبع React Native أثناء pod install سطرين [SPM] WARNING لكل مكتبة تستخدم حزم
Swift: أحدهما يطلب إعادة فتح الـ workspace إن كنت تستخدم Xcode 15 أو أقدم، والآخر
ينبّه إلى أن الحزمة مربوطة ربطاً ثابتاً (static). إن فشل ربط (link) تطبيقك بعد ذلك،
فثبّت باستخدام frameworks ديناميكية: USE_FRAMEWORKS=dynamic pod install.
كانت الإصدارات السابقة تأخذ iOS SDK من CocoaPods (الـ pod Joryio/UI). إن كنت قد
أضفت بنفسك سطر pod 'Joryio...' إلى Podfile - مثلاً pod بـ :path يشير إلى نسخة
محلية - فاحذفه ثم شغّل pod install مجدداً.
إعداد Android
لا شيء لتضيفه. تُربط الحزمة تلقائيًا (autolinking - فهي تتضمن
react-native.config.js)، لذا يسجّل PackageList الحزمة JoryioPackage مسبقًا -
لا تضفها يدويًا أيضًا في MainApplication، لأن ذلك يسجّل الوحدة مرتين. يعتمد الجسر
على io.joryio:joryio-android-ui من Maven Central، بالإصدار نفسه للحزمة.
Expo
الـ SDK وحدة أصلية (native module)، لذا لا يعمل في Expo Go - استخدم development build (npx expo prebuild أو EAS Build) على Expo SDK 52 أو أحدث. قد يكون هدف النشر (deployment target) الافتراضي لـ iOS في Expo أقل من 14.0 الذي يحتاجه الـ SDK؛ ارفعه باستخدام expo-build-properties في app.json:
{
"expo": {
"plugins": [["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }]]
}
}
بدء سريع
import { Platform } from 'react-native';
import Joryio from '@joryio/react-native-sdk';
// التهيئة مرة واحدة في App.tsx
await Joryio.initialize(
Platform.select({
ios: 'jry_sdk_ios_YOUR_IOS_KEY', // مفتاح iOS من لوحة التحكم
android: 'jry_sdk_android_YOUR_ANDROID_KEY', // مفتاح Android من لوحة التحكم
})!,
'api-eu1.joryio.com', // مضيف الـ API (اسم المضيف فقط، دون بروتوكول)
{
enableDebug: __DEV__,
trackSessionStart: true,
}
);
لكل منصة مفتاح SDK خاص بها: تبدأ مفاتيح iOS بـ jry_sdk_ios_ ومفاتيح Android بـ jry_sdk_android_. المفتاح الذي ترفضه المنصة يجعل initialize() يسجّل تحذيرًا ويترك الـ SDK متوقفًا حتى تستدعيه مرة أخرى بالمفتاح الصحيح.
تتبع الأحداث
تتبع أحداث مخصصة
// Basic event
Joryio.track('Button Clicked');
// Event with properties
Joryio.track('Product Added', {
productId: 'SKU-123',
productName: 'Blue T-Shirt',
price: 29.99,
currency: 'USD',
});
// التجارة الإلكترونية: استخدم Joryio.ecommerce (انظر "التجارة الإلكترونية" أدناه) - فهو يرسل الأسماء والمفاتيح التي تقرؤها تقارير الإيرادات
تتبع مشاهدات الشاشات
// In your screen components
Joryio.trackScreen('ProductDetail', { productId: 'SKU-123' });
Joryio.trackScreen('Cart');
Joryio.trackScreen('Checkout');
دمج React Navigation
import { useRef } from 'react';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
function App() {
const navigationRef = useNavigationContainerRef();
const routeNameRef = useRef<string>();
return (
<NavigationContainer
ref={navigationRef}
onStateChange={() => {
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName && currentRouteName !== routeNameRef.current) {
Joryio.trackScreen(currentRouteName);
routeNameRef.current = currentRouteName;
}
}}
>
{/* ... */}
</NavigationContainer>
);
}
هوية المستخدم
تعريف المستخدمين
أرسل أرقام الهاتف دائمًا بالصيغة الدولية الكاملة (E.164): + ثم رمز الدولة ثم الرقم - مثل +972501234567. الرقم الذي لا يبدأ بـ + يُقرأ وفق الدولة الافتراضية لأرقام الهاتف في مساحة العمل؛ وإذا لم تُعيَّن، أو أمكن قراءة الأرقام بطريقتين، يُحفظ غير موثّق ولا يُستخدم للرسائل. كيف تُعالَج أرقام الهاتف
استدع identify بعد تسجيل الدخول أو حين تعرف هوية المستخدم:
// After login
Joryio.identify('user-123');
// With attributes
Joryio.identify('user-123');
Joryio.setAttributes({
firstName: 'John',
plan: 'premium',
});
المعرّفات البديلة (Aliases)
أرفق معرّفًا من نظام آخر - معرّف CRM أو رقم برنامج ولاء - بالشخص الحالي. يعمل ذلك مع الزوار المجهولين أيضًا: يُحفظ المعرّف البديل مع المعرّف المجهول ويبقى مع الشخص بعد تعريفه.
Joryio.addAlias('crm_id', 'C-1042');
لا توجد alias(userId) بلا تسمية. لتسمية الشخص الحالي استدعِ identify(userId).
إعادة الضبط عند تسجيل الخروج
امسح هوية المستخدم وابدأ جلسة مجهولة جديدة:
Joryio.reset();
سمات المستخدم
// Set multiple attributes
Joryio.setAttributes({
firstName: 'John',
lastName: 'Doe',
plan: 'premium',
age: 28,
isVIP: true,
});
// Set a single attribute
Joryio.setAttribute('favoriteColor', 'blue');
// Increment a numeric attribute
Joryio.incrementAttribute('loginCount', 1);
Joryio.incrementAttribute('totalSpent', 29.99);
// Remove an attribute
Joryio.unsetAttribute('temporaryFlag');
القيمة null تحذف السمة على المنصتين: setAttribute('nickname', null) وsetAttributes({ nickname: null }) يفعلان ما تفعله unsetAttribute('nickname').
التجارة الإلكترونية
تتبّع عمليات الشراء والخطوات التي تسبقها باستخدام Joryio.ecommerce. فهو يرسل أسماء الأحداث ومفاتيح الخصائص القياسية التي تقرؤها تقارير الإيرادات في Joryio، فلا تكتب أيًا منها يدويًا:
Joryio.ecommerce.viewProduct({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99 });
Joryio.ecommerce.addToCart({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 });
Joryio.ecommerce.startCheckout(
[{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
59.98, // قيمة السلة
);
// يُرسَل بوصفه "Order Completed" مع orderId وtotal - المفتاحان اللذان تقرؤهما تقارير الإيرادات
Joryio.ecommerce.purchase({
orderId: 'ORD-789',
items: [{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
value: 59.98,
currency: 'USD',
});
يرسل purchase حدث Order Completed يحمل orderId وtotal (قيمة value للطلب) - المفتاحين اللذين تقرؤهما الإيرادات والإسناد. أما استدعاء يدوي مثل track('Order Completed', { order_id, value }) فيُخزَّن كما كُتب ويُحتسب طلبًا بقيمة 0 بلا معرّف طلب: لا توجد عمدًا طبقة أسماء بديلة تربط التهجئات الأخرى. إذا تتبّعت عملية شراء بنفسك، فاستخدم orderId وtotal.
الاستدعاءات الأخرى: viewCategory(categoryId, categoryName), search(query, resultCount?), removeFromCart(product, quantity?), cartViewed(items, cartValue), updateCart(items, cartValue), addPaymentInfo(paymentMethod), orderFulfilled(orderId, trackingNumber?, carrier?), orderDelivered(orderId), orderCancelled(orderId, reason?), orderRefunded(orderId, refundAmount?, reason?), addToWishlist(product), removeFromWishlist(product), shareProduct(product, shareMethod), applyCoupon(couponCode, discountAmount?, discountType?), removeCoupon(couponCode), submitReview(productId, rating, reviewText?). ecommerce متاح أيضًا كتصدير مسمّى: import { ecommerce } from '@joryio/react-native-sdk'.
الاستدعاء الذي ينقصه وسيط إلزامي - purchase دون orderId أو items أو value رقمية، أو منتج دون productId، أو سلة دون items - يسجّل تحذيرًا ولا يرسل شيئًا، فلا يُسجَّل أبدًا كطلب بقيمة 0.
إشعارات Push
اطلبوا الإذن عندما يكون لدى المستخدم سبب للموافقة. الطلب في الثواني الأولى من الجلسة هو الأكثر رفضًا، والتراجع عن الرفض أصعب بكثير من التأخير - فطلب النظام يظهر مرة واحدة فقط، ولذلك فإن "لا" هناك دائمة عمليًا، بينما الطلب لاحقًا لا يكلفكم شيئًا.
ينطبق هذا أيًا كان المكوّن الذي يطلب الإذن. إذا كان Firebase Messaging هو من
يعرض الطلب، كما في الأسفل، فالتوقيت بين أيديكم؛ وإذا عرضه SDK الخاص بـ Joryio
فهو لا يطلب نيابةً عنكم إلا إذا فعّلتم requestPushPermissionAtLaunch، وهو
معطّل افتراضيًا وليس غير متاح.
الإعداد باستخدام Firebase، React Native Firebase
import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';
// Request permission
const authStatus = await messaging().requestPermission();
// Get and register token
// يرسل Joryio إشعارات iOS عبر APNs، لذا سجّل على iOS رمز APNs وليس رمز FCM.
const token = Platform.OS === 'ios'
? await messaging().getAPNSToken()
: await messaging().getToken();
if (token) {
const result = await Joryio.registerPushToken(token);
if (!result.success && result.isRetryable) {
// عطل مؤقت (بلا اتصال، DNS، 5xx): سجّل مرة أخرى عند عودة الشبكة
}
}
// Listen for token refresh
messaging().onTokenRefresh((newToken) => {
// يسلّم onTokenRefresh رمز FCM - على iOS أيضًا، عند كل تشغيل للتطبيق.
// سجّله على Android فقط؛ على iOS الرمز الصحيح هو رمز APNs أعلاه.
if (Platform.OS === 'android') Joryio.registerPushToken(newToken);
});
على iOS سجّل رمز جهاز APNs فقط. يسلّم onTokenRefresh في React Native Firebase رمز FCM على iOS أيضًا، عند كل تشغيل للتطبيق، وتسجيله يستبدل رمز APNs الصالح للجهاز برمز يرفضه APNs - لذا سجّله على Android فقط، كما في المثال أعلاه. على iOS يرفض registerPushToken أي رمز ليس رمز APNs (ست عشري) ويُرجع success: false وisRetryable: false.
إذا أوقف المستخدم الإشعارات واستدعيت unregisterPush()، فلا تسجّل رموزاً من الاستدعاءات التلقائية (onTokenRefresh، أو getToken() عند الإقلاع) حتى يعيد تشغيلها: registerPushToken() هو "تشغيل الإشعارات" الصريح ويلغي إلغاء التسجيل. تحقّق من إعداد الإشعارات لديك قبل التسجيل.
يعيد registerPushToken وعدًا بالنتيجة (success وmessage وisUnauthorized وisRetryable). لا يُرفض أبدًا، لذا فإن تجاهل الوعد يحافظ على السلوك السابق - لكن العطل المؤقت يترك الجهاز غير مسجّل حتى التشغيل التالي، فتحقق من isRetryable وسجّل مرة أخرى. يحمل getDiagnostics().lastError علامة isRetryable نفسها.
معالجة نقرات إشعارات Push
import messaging from '@react-native-firebase/messaging';
// When app is in background and notification is tapped
messaging().onNotificationOpenedApp((remoteMessage) => {
const trackingId = remoteMessage.data?.trackingId;
if (trackingId) {
Joryio.trackPushClick(trackingId);
}
});
// When app was killed and opened via notification
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage?.data?.trackingId) {
Joryio.trackPushClick(remoteMessage.data.trackingId);
}
});
إيصالات التسليم
عندما تستقبل مكتبة الرسائل الخاصة بك الإشعار (React Native Firebase أو Expo notifications)، لا تراه الحزمة الأصلية أبدًا، لذا أبلغ عن التسليم بنفسك باستخدام trackingId من بيانات الرسالة:
messaging().onMessage(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});
messaging().setBackgroundMessageHandler(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});
إلغاء التسجيل
Joryio.unregisterPush();
يوقف unregisterPush() الإشعارات إلى هذا الجهاز. ويبقى ساريًا بعد إعادة التشغيل حتى تستدعي registerPushToken() مرة أخرى.
فحص حالة Push
const enabled = await Joryio.isPushEnabled();
console.log('Push enabled:', enabled);
طلب الإذن
if ((await Joryio.getPushPermissionStatus()) === 'notDetermined') {
// اعرض شرحك الخاص أولًا، ثم:
const status = await Joryio.requestPushPermission(); // 'granted' | 'denied' | 'notDetermined' | 'unavailable'
}
يتصرف requestPushPermission() بالطريقة نفسها على iOS وAndroid:
- ينتهي دائمًا. على Android، إذا أُعيد إنشاء الـ Activity أثناء ظهور نافذة الإذن (تدوير الشاشة أو تغيير السمة)، يعيد الحالة كما هي عند عودة التطبيق إلى الواجهة.
- عندما يكون المستخدم قد ألغى الموافقة على التتبّع، تظهر نافذة الإذن مع ذلك وتُعاد النتيجة - فقرار السماح للتطبيق بالإشعارات هو اختيار المستخدم في نظام التشغيل - لكن لا يُسجَّل شيء ولا يُبلَّغ Joryio بشيء ما دام الإلغاء قائمًا.
رسائل In-app
تُعرض الرسائل من تلقاء نفسها. ثبّت الحزمة وأرسل حملة فتظهر - ترسمها العناصر الأصلية بخطوط تطبيقك وألوانه ووضعه الداكن. لا شيء لتوصيله.
لن تحتاج بقية هذا القسم إلا إذا أردت عرض الرسائل داخل React بدلًا من ذلك؛ راجع تولي العرض بنفسك.
رموز التسليم (delivery tokens)
عندما يقدّم الخادم حملة مؤهّلة، فإنه يصدر لها أيضًا رمز تسليم موقّعًا وقصير الأجل. يعيد الـ SDK إرسال هذا الرمز عند الإبلاغ عن ظهور أو نقرة أو إغلاق، ويتحقق الخادم من التوقيع قبل تسجيل أي شيء.
لا يتعيّن عليك فعل أي شيء - الـ SDK يتولى ذلك نيابة عنك. وهو موثّق هنا لأن الخادم يمكن ضبطه ليشترط الرمز. افتراضيًا يظل التقرير الذي لا يحمل رمزًا مقبولًا - ويسجّله الخادم كعميل من إصدار سابق للرموز - ولا يُرفض التقرير نفسه إلا عندما يعمل الخادم بالإعداد IN_APP_REQUIRE_DELIVERY_TOKEN=true:
POST /v1/in-app/track (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }
أما الرمز الموجود لكنه لا يجتاز التحقق - مزوّر أو معدّل أو منتهي الصلاحية - فيُرفض دائمًا أيًا كان الإعداد.
الرمز هو ما يجعل الظهور جديرًا بالثقة: بدونه، يستطيع أي شخص يملك مفتاح الـ SDK - وهو مضمّن في كل تطبيق وكل صفحة - الإبلاغ عن ظهورات ونقرات لحملة لم تُعرض قط، وستحتسبها تقاريرك.
إذا توقّف تسجيل ظهورات in-app على خادم يشترط الرموز، فتأكد من أن التطبيق مبني على إصدار حديث من الـ SDK: الإصدار الذي بُني قبل وجود رموز التسليم لا يرسل رمزًا.
نوعان من المحتوى
تحمل كل رسالة الحقل kind الذي يحدد شكلها. تفرّع بناءً عليه:
kind | ما يحمله | كيفية عرضه |
|---|---|---|
'native' | title وbody وimageUrl وbuttons | مكوّنات React Native - <Text> و<Image> و<Pressable> |
'html' | html وcss | WebView |
تولي العرض بنفسك
الاشتراك عبر onInAppMessage يوقف رسم الحزمة ويسلّم كل رسالة إلى شيفرتك
لتعرضها بمكوّنات React Native. ولن تحصل على نسختين.
صرّح بما يستطيع العارض الخاص بك رسمه:
Joryio.onInAppMessage(handleMessage, {
capabilities: ['content.native'], // أضف 'content.html' فقط إذا كان لديك web view
});
تُستهدف الحملات وفق ما يبلّغ عنه الجهاز، والقدرة غير المُصرَّح بها تُقرأ على أنها "لا يستطيع"، لا "غير معروف". حذف هذا الخيار يُبقي السلوك السابق، حيث كانت القيمة تأتي من إعداد أصلي يصف واجهات الحزمة الأصلية نفسها - وهي لا تعمل أصلًا بمجرد اشتراكك.
افعل ذلك إن أردت أن تطابق الرسائل بقية واجهتك. الجسر نفسه يعتمد على حزم الواجهة
الأصلية (io.joryio:joryio-android-ui على أندرويد، والمنتج JoryioUI من حزمة Swift على iOS)،
لذا تبقى مرتبطة؛ الاشتراك يوقف رسمها فقط.
إزالة اشتراكك الأخير تعيد العرض إلى الـ SDK الأصلي، فتُعرض الرسائل التي تصل بعد ذلك عرضًا أصليًا بدل أن تضيع. وقائمة capabilities الفارغة تعني ما يعنيه حذفها (غير مُصرَّح بها) على المنصتين.
إجراءات الأزرار هي dismiss وurl وdeep_link وrequest_push_permission - زر "نعم" في رسالة تمهيدية للإشعارات: أغلق الرسالة واستدعِ Joryio.requestPushPermission().
متى تشترك
منذ الإصدار 1.3.2. اشترك قبل أن يكتمل initialize() - على مستوى الوحدة، أو في effect يعمل عند بدء التشغيل - ولن تُرسم أي رسالة بواسطة الحزمة الأصلية، ولا حتى الأولى: تحتفظ الحزمة برسائل In-App حتى يكتمل initialize() ويصل اشتراكك إلى الحزمة الأصلية، ثم تسلّم الرسالة المحتجزة إلى المستمع الخاص بك، ولا تعيد العرض إلى الحزمة الأصلية إلا إذا لم يشترك أحد.
إذا اشتركت لاحقًا - في شاشة تُحمَّل بعد اكتمال initialize() - فمرّر holdInAppMessagesForHandler: true إلى initialize(). عندها تُحتجز الرسائل الجاهزة قبل اشتراكك (لا تُرسم، ولا يُبلغ عنها، ولا تُحتسب ضمن أي حد للتكرار) وتُسلَّم إلى المستمع عند اشتراكه. أما استدعاء disableInAppMessages() بدلًا من ذلك فينهي الاحتجاز وترسم الحزمة الأصلية الرسالة المحتجزة.
على Android يمرّر الجسر الـ Activity الحالي إلى الـ SDK الأصلي عند initialize()، فتظهر رسائل In-app منذ الجلسة الأولى.
الاستماع للرسائل
import { useEffect, useState } from 'react';
import Joryio, { type InAppMessage } from '@joryio/react-native-sdk';
function App() {
const [message, setMessage] = useState<InAppMessage | null>(null);
useEffect(() => Joryio.onInAppMessage(setMessage), []);
return <>{message && <InAppMessageHost message={message} onClose={() => setMessage(null)} />}</>;
}
عرض رسالة أصلية
المحتوى الأصلي نص وليس markup. ضعه داخل <Text> - فتمريره إلى WebView أو إلى dangerouslySetInnerHTML يعيد خطر الحقن ذاته الذي يتجنبه المسار الأصلي.
function InAppMessageHost({ message, onClose }: { message: InAppMessage; onClose: () => void }) {
// أبلغ عن 'displayed' مرة واحدة، عند ظهور الرسالة.
useEffect(() => {
Joryio.trackInAppImpression(message.id, 'displayed');
}, [message.id]);
// أبلغ عن 'dismissed' كلما غادرت الرسالة الشاشة - بعد النقر أيضًا.
const close = () => {
Joryio.trackInAppImpression(message.id, 'dismissed');
onClose();
};
if (message.kind === 'native') {
return (
<View>
{message.imageUrl && <Image source={{ uri: message.imageUrl }} />}
{message.title && <Text style={styles.title}>{message.title}</Text>}
<Text>{message.body}</Text>
{message.buttons.map((button) => (
<Pressable
key={button.id}
onPress={() => {
Joryio.trackInAppImpression(message.id, 'clicked', button.id);
if (button.action === 'url' && button.url) Linking.openURL(button.url);
if (button.action === 'request_push_permission') void Joryio.requestPushPermission();
close();
}}
>
<Text>{button.text}</Text>
</Pressable>
))}
</View>
);
}
// هرّب `</` حتى لا يتمكن CSS الحملة من إغلاق وسم <style>.
const css = message.css.replace(/<\//g, '<\\/');
return <WebView source={{ html: `<style>${css}</style>${message.html}` }} />;
}
طبِّق تنسيق الحملة
يحمل message.style التجاوزات التي ضبطها محرّر الحملة. كل حقل اختياري، والحقل
الغائب يعني وراثة مظهر تطبيقك - فطبِّق الموجود فقط دون بدائل من عندك:
const st = message.style;
const text = {
// 'auto' يحاذي حسب لغة الرسالة لا لغة الجهاز.
writingDirection: 'auto' as const,
textAlign: st?.textAlign === 'center' ? 'center' : 'auto',
...(st?.fontFamily ? { fontFamily: st.fontFamily } : {}),
};
طبِّق fontFamily فقط إن كان تطبيقك يرفق ذلك الخط. وإن ضبطت الحملة لون زر دون
لون نص، فاختر الأسود أو الأبيض حسب التباين - فالأبيض الافتراضي يُخفي لونًا فاتحًا.
السماح برسائل HTML
رسائل HTML معطلة افتراضيًا. تعرض الحزمة الأصلية رسالة HTML في web view داخل تطبيقك. لا تعمل السكربتات المكتوبة في الرسالة هناك - يعمل جسر الحزمة وحده، وهو يتولى الأزرار والنماذج - لكن تفعيل web view يبقى قرارًا يتخذه فريق التطبيق:
await Joryio.initialize(sdkKey, apiHost, {
allowHtmlJsInAppMessages: true, // الافتراضي: false
});
تركه معطلًا لا يعطل الرسائل داخل التطبيق - تستمر الرسائل الأصلية في الوصول. تُتجاهل حملات HTML فقط.
ينطبق ذلك ما دامت الحزمة هي التي ترسم. وبمجرد استدعاء enableInAppMessages(['content.native', 'content.html'])، يصبح كود JS لديك هو من يرسم كل رسالة ولم يعد هذا الإعداد يحجب HTML - فما تصرّح به في ذلك الاستدعاء هو الموافقة.
تتبع مرات الظهور
// When message is displayed
Joryio.trackInAppImpression(message.id, 'displayed');
// When user clicks
Joryio.trackInAppImpression(message.id, 'clicked', button.id);
// When user dismisses
Joryio.trackInAppImpression(message.id, 'dismissed');
أبلغ عن 'displayed' مرة واحدة عند ظهور الرسالة. هذا البلاغ الواحد هو الذي يسجّل الظهور وهو الذي يُحتسب ضمن حدود تكرار الحملة - تسليم الرسالة إلى الكود الخاص بك لا يفعل أيًا منهما، ولا تبلغ الحزمة عنه نيابة عنك. عند النقر، مرّر معرّف الزر أو الرابط وسيطًا ثالثًا، elementId، كي تتمكن التقارير من معرفة الزر الذي نُقر؛ واحذفه في displayed وdismissed.
أبلغ عن 'dismissed' كلما غادرت الرسالة الشاشة، بما في ذلك بعد النقر. حتى ذلك الحين تحجز الرسالة خانة العرض: على iOS لا تُعرض أي رسالة In-app أخرى، وعلى Android لا تُعرض الحملة نفسها مرة أخرى. وإذا لم يصل 'dismissed' أبدًا، يحرّر الـ SDK الخانة بعد 5 دقائق.
النماذج في رسائل HTML
عندما تعرض رسالة HTML بنفسك، أرسل نموذج <form data-jry-form> الموجود فيها عبر الحزمة، كي تُرسَل القيم مع الهوية الحالية ويُتحقَّق منها مقابل النموذج الذي تحمله الحملة:
// { ok, message?, redirect?, errors? } - لا يُرفض أبدًا؛ الفشل هو ok: false
المزامنة والاختبار
Joryio.syncInAppCampaigns(); // اجلب الحملات الآن واعرض المؤهَّل منها
Joryio.resetDisplayedCampaigns(); // للاختبار فقط: انسَ ما عرضه هذا الجهاز
تزامن الحزمة أصلًا عند بدء الجلسة وعند عودة التطبيق إلى الواجهة؛ استدعِ syncInAppCampaigns() للّحظات التي بينهما. لا يعمل resetDisplayedCampaigns() إلا عند تفعيل enableDebug.
enableInAppMessages(capabilities?) وdisableInAppMessages() هما الزوج منخفض المستوى الذي يستدعيه onInAppMessage نيابة عنك؛ استخدمهما فقط إذا كنت تستمع إلى حدث JoryioInAppMessage بطريقة أخرى. عند إعادة تحميل JavaScript، يعيد الجسر العرض إلى الحزمة الأصلية حتى يشترك الكود الخاص بك مجددًا.
متى تُحتسب الرسالة معروضة
تُحتسب الرسالة معروضة - أي يُرسَل in_app.displayed ويُستهلك حد التكرار للحملة - فقط عندما يكون الشخص قد تمكّن فعلًا من رؤيتها. المدة التي يجب أن تبقى فيها على الشاشة هي IN_APP_DISPLAY_DWELL_MS (1000 مللي ثانية)، وهو الثابت نفسه في كل SDK من Joryio.
- الرسائل التي يرسمها الـ SDK الأصلي تتبع قاعدة iOS وAndroid: تُعرض فقط على شاشة مستقرة في الواجهة، وتُحتسب بعد ثانية واحدة على الشاشة والتطبيق في الواجهة. النقر أو الإغلاق قبل ذلك يُحتسب مشاهدة؛ والرسالة التي تغادر الشاشة أبكر لا تُحتسب وتبقى مؤهلة.
- إذا كنت ترسم الرسائل بنفسك (
onInAppMessage)، أرسل'displayed'وفق القاعدة نفسها -IN_APP_DISPLAY_DWELL_MSمُصدَّر: بعد أن تبقى الرسالة ظاهرة طوال هذه المدة، أو مباشرة قبل نقر أو إغلاق يأتي أبكر. لا ترسله لرسالة غادرت الشاشة قبل ذلك.
الموافقة على التتبّع
مفتاحان منفصلان يحددان ما إذا كان الـ SDK يجمع أي شيء، وأي "لا" هي الغالبة:
- الموافقة - إجابة نافذة الموافقة لديك:
'granted'أو'denied'أو'pending'. تُضبط عبرtrackingConsentفيinitialize()أو عبرupdateConsent(). - إلغاء الموافقة (opt-out) - اختيار الشخص نفسه. يُضبط عبر
optOut()ولا يُرفع إلا عبرoptIn().
لا يجمع الـ SDK شيئًا إلا والموافقة granted والشخص لم يُلغِ موافقته. منح الموافقة لا يلغي أبدًا إلغاء الموافقة، وإلغاء الموافقة لا يغيّر أبدًا الموافقة المحفوظة.
إيقاف الجمع واستئنافه. هذه موافقة على التتبّع وليست اشتراكًا تسويقيًا - optOut() لا يلغي اشتراك أحد في البريد أو الرسائل النصية. تلك تعيش في حالة اشتراك جهة الاتصال.
// رفض المستخدم التتبّع
Joryio.optOut();
// ...ثم وافق لاحقًا
Joryio.optIn();
const optedOut = await Joryio.isUserOptedOut(); // مفتاح إلغاء الموافقة فقط
const consent = await Joryio.getTrackingConsent(); // 'granted' | 'denied' | 'pending'
يمحو wipeData() كل ما يحتفظ به الجهاز للشخص الحالي - الهوية والأحداث في قائمة الانتظار والكتابات المعلّقة وحالة الرسائل داخل التطبيق والجلسة ومعرّف الجهاز ورمز الإشعارات - ويلغي تسجيل الرمز في الخادم. يبقى إلغاء الاشتراك محفوظًا. وتبقى الإشعارات متوقفة حتى registerPushToken() أو منح الإذن.
يسجّل optOut() القرار في الملف الشخصي بوصفه tracking_opted_out قبل أن يتوقف عن الإرسال، بحيث يمكن لجمهور أن يستبعد من رفض. وoptIn() يمسحه. إنه إشارة للاستهداف وليس إنفاذًا.
الأحداث المصطفّة ولم تُرسل بعد تُهمَل عند optOut() ولا تُحفظ لوقت لاحق.
الموافقة عند بدء التشغيل
لاتخاذ القرار قبل إرسال أي شيء، مرّر الموافقة إلى initialize() وحدّثها من نافذة الموافقة لديك:
await Joryio.initialize(sdkKey, apiHost, { trackingConsent: 'pending' });
// لاحقًا
Joryio.updateConsent('granted'); // 'granted' | 'denied' | 'pending'
- اترك
trackingConsentدون قيمة، إلا إذا كانت لدى نافذة الموافقة إجابة لهذا التشغيل. دون قيمة ودون شيء محفوظ، يكون السلوك كما فيgranted؛ أماdeniedالمحفوظة من تشغيل سابق فتبقى سارية. - تُحفظ
deniedعلى الجهاز وتبقى بعد إعادة التشغيل. لا يرفعها إلاupdateConsent('granted')أوtrackingConsent: 'granted'صريح. - تنطبق
pendingعلى هذا التشغيل فقط ولا تحل أبدًا محلdeniedمحفوظة. ما دامت الموافقةpendingلا يُرسَل شيء: تنتظر الأحداث وتغييرات السمات على الجهاز (حتىmaxQueueSize)، ولا يحدث بدء جلسة ولا مزامنة لرسائل In-app ولا تسجيل للإشعارات.updateConsent('granted')يرسل ما انتظر، وupdateConsent('denied')يتجاهله. - مرّر
optOut: trueلتبدأ بإلغاء الموافقة. يُحفظ الإلغاء على الجهاز ويبقى بعد إعادة التشغيل؛ وoptOut: falseلا يتراجع عنoptOut()سابق - استدعِoptIn(). - إذا كان تطبيقك يعرف الإجابة مسبقًا عند بدء التشغيل - مثل اختيار من نافذة الموافقة حفظه التطبيق بنفسه - فمرّرها كـ
trackingConsentإلىinitialize()بدل استدعاءupdateConsent()بعده، حتى يبدأ الـ SDK وهذه الإجابة سارية. حالة الأحرف لا تهم (تُقرأ'Denied'على أنها'denied')؛ وتُتجاهل القيمة غير المعروفة مع تحذير وتبقى الموافقة دون قيمة.
قرارات قبل initialize()
يمكن استدعاء optOut() وoptIn() وupdateConsent() وwipeData() قبل initialize()، وتُطبَّق أولًا: لا يخرج قبلها أي بدء جلسة أو مزامنة لرسائل In-app أو تسجيل للإشعارات. والاستدعاءات السابقة التي تجمع بيانات - الأحداث والسمات والمعرّفات البديلة وتغييرات الاشتراك - يُسقطها optOut() أو updateConsent('denied') أو wipeData() يأتي بعدها، بدل أن تُرسَل عند بدء الـ SDK. وتُسقَط كذلك هذه الاستدعاءات إذا جرت أثناء إلغاء الاشتراك في التتبع أو رفض الموافقة - قبل initialize() أيضًا، سواء اتُّخذ القرار في هذا التشغيل أو حُفظ من تشغيل سابق - ولا يُرسلها optIn() أو updateConsent('granted') لاحق. ولا يُسقِط identify إلا wipeData(). أما setSdkAuthenticationToken() السابق فلا ينتقل أبدًا إلى الزائر التالي: على iOS وAndroid على حد سواء يحتفظ به المسح لعمليات الانسحاب الخاصة بالشخص الذي مُسحت بياناته وحدها، أو يُسقطه. بعد optOut() أو الرفض يُحفظ معرّف المستخدم على الجهاز دون إرساله، فقط كي يصل إلغاء الاشتراك إلى الشخص الصحيح. ويُستثنى الانسحاب: يُحتفظ باستدعاءات setSubscription(channel, 'unsubscribed') وremoveFromSubscriptionGroup وunregisterPush() السابقة وتُرسَل.
القرار الذي يُتخذ أثناء تشغيل initialize() ثم يفشل يُطبَّق لحظة الفشل، قبل initialize() التالي: تبدأ إعادة المحاولة والقرار ساري المفعول.
يمكن أيضًا استدعاء isUserOptedOut() وgetTrackingConsent() قبل initialize(). أثناء تشغيل initialize() تنتظرانه؛ وفي غير ذلك - قبل initialize() وبعد استدعاء فشل - تجيبان فورًا مما هو محفوظ على الجهاز، لذا يظهر الإلغاء أو denied المحفوظان من تشغيل سابق فورًا. وإذا لم يُتخذ قرار ولم يُحفظ شيء، تجيب getTrackingConsent() بـ 'granted'، وهي الحالة التي يعمل بها الـ SDK، على المنصتين.
خيارات الإعداد
| الخيار | النوع | الافتراضي | الوصف |
|---|---|---|---|
enableDebug | boolean | false | تفعيل سجلات التصحيح |
logLevel | string | مشتق من enableDebug | مستوى السجل: debug أو info أو warn أو error. إن تُرك دون ضبط، يستخدم الجسران الأصليان 'debug' عندما يكون enableDebug صحيحًا و'error' خلاف ذلك - لا يوجد افتراضي ثابت. اضبطه فقط لتجاوز ذلك. |
batchSize | number | 50 | الأحداث في كل دفعة قبل التفريغ التلقائي، الافتراضي الأصلي |
flushInterval | number | 5000 | فاصل التفريغ التلقائي بالـms، الافتراضي الأصلي |
sendImmediately | boolean | false | إرسال كل حدث فور تتبعه دون تجميع في دفعات، غير موصى به للإنتاج |
maxQueueSize | number | 1000 | أقصى عدد أحداث تحتفظ به قائمة الانتظار المحلية أثناء فشل الإرسال؛ بعده تُحذف الأقدم، الافتراضي الأصلي |
requestTimeout | number | 10000 | مهلة HTTP لكل طلب بالـms، الافتراضي الأصلي |
sessionTimeout | number | 1800000 | مهلة الجلسة بالـms، 30 دقيقة افتراضيًا |
trackSessionStart | boolean | true | تتبع أحداث بدء الجلسة تلقائيًا |
userId | string | null | User ID مضبوط مسبقًا عند التهيئة |
anonymousId | string | يُولَّد تلقائيًا | معرّفك المجهول الخاص لهذا الجهاز. يُستخدم لهذا التشغيل ويُخزَّن فقط عندما لا يوجد شيء مخزَّن بعد، فيبقى المعرّف المضبوط مرة واحدة حتى لو توقف التطبيق عن تمرير الخيار |
requestPushPermissionAtLaunch | boolean | false | طلب إذن الإشعارات أثناء initialize. الأفضل أن تطلبه في لحظة يفهم فيها المستخدم السبب |
allowHtmlJsInAppMessages | boolean | false | السماح للحزمة الأصلية برسم رسائل HTML داخل التطبيق (انظر السماح برسائل HTML) |
holdInAppMessagesForHandler | boolean | false | منذ الإصدار 1.3.2. تطبيقك يرسم رسائل In-App ويشترك بعد اكتمال initialize(): تُحتجز الرسائل حتى يشترك (انظر متى تشترك) |
enableSdkAuthentication | boolean | false | إرفاق JWT تصدره أنت بكل طلب إرسال بيانات |
sdkAuthenticationToken | string | - | رمز JWT الأولي عند تفعيل enableSdkAuthentication |
trackingConsent | 'granted' | 'denied' | 'pending' | دون قيمة | الموافقة على التتبّع عند بدء التشغيل. اتركها دون قيمة إلا إذا كانت لديك إجابة من نافذة الموافقة - انظر الموافقة عند بدء التشغيل |
optOut | boolean | false | البدء مع إلغاء الموافقة على التتبّع |
notificationChannelName | string | "Notifications" | Android فقط: اسم قناة الإشعارات الذي يراه المستخدمون في إعدادات النظام (Android 8 فما فوق). مرّره مترجمًا. يُتجاهل على iOS |
أدوات مساعدة
// Flush events immediately (before app close, logout, etc.)
Joryio.flush();
// Get IDs
const anonymousId = await Joryio.getAnonymousId();
const userId = await Joryio.getUserId(); // null if not identified
const sessionId = await Joryio.getSessionId();
دعم TypeScript
SDK مكتوب بالكامل بالأنواع. استورد الأنواع عند الحاجة:
import Joryio, {
type JoryioConfig,
type UserAttributes,
type InAppMessage,
} from '@joryio/react-native-sdk';
الاشتراك التسويقي
ما إذا كان مسموحًا بمراسلة هذا الشخص على قناة ما. هذه موافقة مختلفة عن optOut() التي تخص ما إذا كان مسموحًا بتتبّعه - ولا تستلزم إحداهما الأخرى.
هذا ما يكتبه مركز التفضيلات داخل التطبيق.
Joryio.setSubscription('email', 'unsubscribed');
مجموعات الاشتراك قوائم يمكن للشخص الانضمام إليها أو مغادرتها لكل قناة:
Joryio.addToSubscriptionGroup('list_abc123', 'email');
Joryio.removeFromSubscriptionGroup('list_abc123', 'email');
القنوات: email, sms, whatsapp, push, viber. الحالات: optedIn, subscribed, unsubscribed.
المعرّف مطلوب - تُتخطّى المكالمة، مع تسجيل خطأ، إن لم يوجد معرّف مستخدم ولا معرّف مجهول.
مصادقة SDK
اختيارية. عند تفعيلها، يجب على الطلب الذي يحمل معرّف مستخدم أن يحمل أيضًا JWT أصدره الخادم الخاص بك لذلك المستخدم؛ أما استدعاءات الزوار المجهولين فتُقبل بدونه.
await Joryio.initialize(sdkKey, apiHost, {
enableSdkAuthentication: true,
sdkAuthenticationToken: initialToken,
});
const stop = Joryio.onSdkAuthenticationError(async (error) => {
// error.reason: 'missing' | 'invalid' | 'expired' | 'sub_mismatch' | 'unknown'
// error.refreshExhausted: true بمجرد أن تتوقف الحزمة عن إعادة محاولة هذا الطلب
Joryio.setSdkAuthenticationToken(await fetchFreshToken());
});
استدعِ setSdkAuthenticationToken(token) بعد تسجيل الدخول وكلما توفّر لدى المعالج رمز جديد؛ ويُرفَق بكل طلب لاحق. يعيد onSdkAuthenticationError دالة لإلغاء الاشتراك.
التشخيص
const { apiEndpoint, lastError } = await Joryio.getDiagnostics();
// lastError: { message, isUnauthorized, isRetryable, at } - يعود null بمجرد نجاح استدعاء
استخدمه عندما يتصرف الدمج على نحو غير متوقع: مفتاح SDK خاطئ يبدو مهيَّأً بينما يفشل كل طلب مع isUnauthorized: true.
آمن مع null والاستدعاءات المبكرة
- كل استدعاء آمن قبل اكتمال
initialize(): يُحتفظ بما يصل إلى 1,000 استدعاء وتُنفَّذ بالترتيب، وتنتظر دوال القراءة حتى 5 ثوانٍ قبل أن تعيد قيمة افتراضية. وبعد 1,000 يُسقَط الأقدم، واستدعاءات البيانات أولًا: لا يُسقَط إلغاء الاشتراك أوunregisterPush()أو قرار الخصوصية إلا عندما لا يبقى شيء آخر. - لا يتسبب
nullأوundefinedأبدًا في تعطّل تطبيقك. قيمة إعداد null تعني القيمة الافتراضية؛ والاستدعاء الذي ينقصه وسيط إلزامي (اسم حدث، معرّف مستخدم، رمز) يسجّل تحذيرًا ولا يفعل شيئًا؛ وقيمة سمة null تزيل السمة. - إذا فشل
initialize()، يُحتفظ بالاستدعاءات بالطريقة نفسها وتُرسَل عند نجاحinitialize()لاحق - يمكنك استدعاؤه مرة أخرى بقيم مصحّحة. ويُحتفظ بالمستمعين الذين سجّلتهم (onInAppMessageوonSdkAuthenticationError) وبإلغاءات الاشتراك التي تمت في الأثناء، فلا تضيع. وقرار الخصوصية الذي اتُّخذ بينما كانinitialize()لا يزال قيد التشغيل يُطبَّق لحظة الفشل، فتبدأ إعادة المحاولة به. - استدعاءان متداخلان لـ
initialize()- إذ يشغّل React StrictMode التأثيرات مرتين - يتشاركان تشغيلًا واحدًا. ويُتجاهل أي استدعاء لاحق ويبقى الـ SDK عاملًا؛ وتحتاج الإعدادات المعدّلة إلى إعادة تشغيل التطبيق. - لا تُرسَل
NaNوInfinityأبدًا: قيمة السمة أو خاصية الحدث التي تحمل إحداهما تُحذف مع تحذير، ويُنفَّذ باقي الاستدعاء. ويُتجاهلsetAttribute(key, NaN)بدل أن يزيل السمة. وتُفحص أيضًا القيم داخل كائنات أصنافك الخاصة - سطر سلة مبني من صنف، مثلًا. - أي callback تمرّره إلى
onInAppMessageأوonSdkAuthenticationErrorويرمي خطأ (أو يعيد promise يُرفض) يُلتقط ويُسجَّل. لا يتسبب أبدًا في تعطّل تطبيقك ولا يوقف المستمعين الآخرين.
مرجع API
| المجال | الدوال |
|---|---|
| الإعداد | initialize(sdkKey, apiHost, config?) |
| الأحداث | track(eventName, properties?), trackScreen(screenName, properties?), flush() |
| الهوية | identify(userId), addAlias(aliasLabel, aliasName), reset(), getUserId(), getAnonymousId(), getSessionId() |
| السمات | setAttributes(attributes), setAttribute(key, value), incrementAttribute(key, by?), unsetAttribute(key) |
| Push | registerPushToken(token), unregisterPush(), reportPushDelivered(trackingId), trackPushClick(trackingId), getPushPermissionStatus(), requestPushPermission(), isPushEnabled() |
| In-app | onInAppMessage(callback, options?), trackInAppImpression(campaignId, action, elementId?), submitInAppForm(campaignId, values), syncInAppCampaigns(), resetDisplayedCampaigns(), enableInAppMessages(capabilities?), disableInAppMessages() |
| الموافقة | optOut(), optIn(), isUserOptedOut(), getTrackingConsent(), updateConsent(consent), wipeData() |
| الاشتراكات | setSubscription(channel, status), addToSubscriptionGroup(groupId, channel), removeFromSubscriptionGroup(groupId, channel) |
| مصادقة SDK | setSdkAuthenticationToken(token), onSdkAuthenticationError(callback) |
| التجارة الإلكترونية | Joryio.ecommerce.* - انظر التجارة الإلكترونية |
| التشخيص | getDiagnostics() |
الاختبار باستخدام Jest
الحقل main في الحزمة يشير إلى JavaScript مُصرَّف، فلا يحتاج إعداد Jest لديك إلى إدخال transformIgnorePatterns لها. ومن دون محاكاة (mock) لـ NativeModules.JoryioModule تصبح كل استدعاءات SDK بلا أثر، وتعيد دوال القراءة قيمها الافتراضية، ولا يرمي أي استدعاء خطأ.