دمج Android SDK
SDK أصلي لـ Android لتتبع الأحداث وإدارة جلسات المستخدمين وإرسال إشعارات Push عبر FCM وعرض رسائل In-app.
الخصائص
- خفيف: أثر محدود.
- سريع: محسّن للأداء.
- دعم عدم الاتصال: طابور أحداث مبني على SQLite.
- إعادة محاولة تلقائية: تراجع أسي عند الإخفاق.
- دفعات: تجميع فعال للأحداث، 50 حدثًا/5 ثوانٍ.
- رسائل In-app: رسائل أصلية و‑HTML، 5 أنواع، مع حدود تكرار.
- إشعارات Push: Firebase Cloud Messaging، FCM.
- الخصوصية أولًا: متوافق مع GDPR ويحترم موافقة المستخدم.
- Android 6.0+: دعم API 23+.
المتطلبات
- Android 6.0، API level 23، أو أحدث - الحد الأدنى للتخزين المشفّر (
EncryptedSharedPreferences)، وهو نفس الحد الذي يتطلبه Firebase Cloud Messaging. - Kotlin 1.9.20 أو أحدث.
- Java 17 للبناء باستخدام Android Gradle Plugin 8. فئات SDK مُترجمة إلى bytecode لـ Java 11، لذا يُترجَم معها أيضًا بناءٌ يستخدم JDK 11 (Unity 2022.3).
- Gradle 8.0 أو أحدث.
إعداد SDK المحلي
للبُنى المحلية، اضبط مسار Android SDK في local.properties:
sdk.dir=/Users/your-user/Library/Android/sdk
أو صدّر ANDROID_HOME/ANDROID_SDK_ROOT قبل تشغيل Gradle.
التثبيت
Gradle، موصى به
أضف إلى build.gradle.kts:
تأتي الحزمة في artifactين. أضف واحدًا منهما - إذ يحتوي artifact الواجهة على الأساسي، فلا تحتاج مطلقًا إلى الإعلان عنهما معًا:
dependencies {
// كل شيء، بما في ذلك عرض الرسائل داخل التطبيق. ابدأ من هنا.
implementation("io.joryio:joryio-android-ui:1.3.3")
}
dependencies {
// التتبع والهوية والإشعارات فقط - دون عرض داخل التطبيق ودون ربط WebView.
// اختر هذا إذا كان تطبيقك لا يستخدم الرسائل داخل التطبيق، أو إذا كنت تعرضها بنفسك.
implementation("io.joryio:joryio-android:1.3.3")
}
أو باستخدام Groovy (build.gradle):
dependencies {
implementation 'io.joryio:joryio-android-ui:1.3.3'
}
يعتمد joryio-android-ui على joryio-android، لذا يمنحك سطر واحد كليهما ولا
يمكن أن يفترقا - فأنت تذكر إصدارًا واحدًا فقط.
خذ الـ artifact الأساسي وحده إذا كان تطبيقك لا يحتاج الرسائل داخل التطبيق، أو إذا اعترضت مراجعة أمنية على إمكانية ربط WebView أصلًا. عندها لن تُعرض الرسائل داخل التطبيق فحسب، ويعمل كل شيء آخر دون تغيير.
بدء سريع
1. هيئ SDK
في فئة Application:
import io.joryio.sdk.Joryio
import io.joryio.sdk.JoryioConfig
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize with your SDK key
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_SDK_KEY",
apiHost = "https://api-eu1.joryio.com/api"
)
}
}
اعثر على مفتاح SDK في لوحة Joryio ضمن Settings ← Apps ← [Your App] ← SDK Keys.
2. تتبع الأحداث
// Basic event
Joryio.track("Button Tapped")
// Event with properties
Joryio.track("Product Viewed", mapOf(
"product_id" to "abc123",
"product_name" to "Wireless Headphones",
"price" to 99.99,
"category" to "Electronics"
))
// Screen view
Joryio.trackScreen("ProductDetail", mapOf(
"product_id" to "abc123"
))
3. تعريف المستخدمين
أرسل أرقام الهاتف دائمًا بالصيغة الدولية الكاملة (E.164): + ثم رمز الدولة ثم الرقم - مثل +972501234567. الرقم الذي لا يبدأ بـ + يُقرأ وفق الدولة الافتراضية لأرقام الهاتف في مساحة العمل؛ وإذا لم تُعيَّن، أو أمكن قراءة الأرقام بطريقتين، يُحفظ غير موثّق ولا يُستخدم للرسائل. كيف تُعالَج أرقام الهاتف
// Identify a user
Joryio.identify("user-123")
Joryio.getInstance().setAttributes(mapOf(
"name" to "John Doe",
"plan" to "premium"
))
// Set attributes later
Joryio.setAttribute("last_purchase", Date())
Joryio.incrementAttribute("lifetime_value", 99.99)
// Attach another identifier of yours (label: lowercase letters, digits, _).
// Works before identify() too - it follows the visitor through login.
Joryio.addAlias("loyalty_id", "L-12345")
// On logout: forgets the user AND starts a new anonymous ID
Joryio.reset()
identify هو الطريقة التي يتحول بها زائر مجهول إلى مستخدم معروف؛ لا يوجد نداء منفصل alias(userId). يمسح reset() أيضًا كتابات السمات التي لم تُرسل بعد وأي رسالة in-app تنتظر العرض، ويعيد تسجيل رمز Push تحت المعرّف المجهول الجديد ويزامن رسائل in-app من جديد، فلا يُسجَّل الشخص التالي على الجهاز أبدًا على أنه الشخص السابق.
النداءات على Joryio التي تتم قبل انتهاء التهيئة توضع في طابور وتُنفَّذ بالترتيب فور انتهائها، سواء مع initialize أو مع initializeAsync. لا يرمي أي نداء عام استثناءً إلى شيفرتك: الوسيط المفقود (null من Java) يُسجَّل في السجل ويُتجاهل. الاستثناء الوحيد هو Joryio.getInstance() قبل initialize() - انظر أدناه.
بيانات الجلسة التلقائية
يثري Android SDK أحداث Session Start ببيانات الجهاز والبيئة:
$device_id$platform، android$manufacturer/$model$os_name/$os_version/$os_sdk_int$app_version/$build_number$package_name$screen_width/$screen_height$locale$language/$languages$timezonecountry، ISO-3166-1 alpha-2، مشتقة من IP عند بدء الجلسة.
إشعارات Push
اطلبوا الإذن عندما يكون لدى المستخدم سبب للموافقة. الطلب في الثواني الأولى من الجلسة هو الأكثر رفضًا، والتراجع عن الرفض أصعب بكثير من التأخير - فطلب النظام يظهر مرة واحدة فقط، ولذلك فإن "لا" هناك دائمة عمليًا، بينما الطلب لاحقًا لا يكلفكم شيئًا.
الـ SDK لا يعرض الطلب نيابةً عنكم أبدًا. وإذا قررتم أنكم تريدونه عند الإقلاع،
فعّلوا requestPushPermissionAtLaunch - فهو معطّل افتراضيًا وليس غير متاح.
فعّل إشعارات Push باستخدام Firebase Cloud Messaging، FCM.
1. أضف Firebase إلى مشروعك
اتبع دليل إعداد Firebase لإضافة Firebase إلى مشروع Android.
2. أضف Service إلى AndroidManifest.xml
<service
android:name="io.joryio.sdk.push.JoryioFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
3. سجّل لإشعارات Push
import com.google.firebase.messaging.FirebaseMessaging
// Get FCM token and register
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
Joryio.getInstance().registerPushToken(token) { result ->
if (!result.success && result.isRetryable) {
// عطل مؤقت (بلا اتصال، DNS، 5xx): سجّل مرة أخرى عند عودة الشبكة
}
if (result.isUnauthorized) {
// مفتاح SDK خاطئ - إعادة المحاولة لن تفيد
}
}
}
}
// Check if push is enabled
val isEnabled = Joryio.getInstance().isPushEnabled()
// Unregister when needed
Joryio.getInstance().unregisterPush()
مرّر onResult لمعرفة النتيجة: isRetryable يعني عطلًا مؤقتًا (بلا اتصال، DNS، 5xx) يستحق التسجيل مجددًا؛ isUnauthorized يعني أن مفتاح SDK خاطئ. بدونه يكون النداء بلا رد. لا يُحتسب التسجيل إلا عندما يؤكده الخادم؛ والرد success: false يُبلَّغ عنه كإخفاق ويُعاد تسجيله عند العودة التالية إلى الواجهة. بعد unregisterPush()، يؤدي استدعاء registerPushToken() إلى تسجيل الجهاز مجددًا. لا تمرّر إلى registerPushToken() نداءات الرمز التلقائية من Firebase (onNewToken() في خدمة الرسائل الخاصة بك، أو الرمز الذي يُجلب عند كل تشغيل) ما دام المستخدم قد أوقف الإشعارات: فهذا هو النداء الذي يعيد تشغيلها. تتولى خدمة الرسائل الخاصة بـ SDK الرموز الجديدة بنفسها ولا تلغي unregisterPush() أبدًا، ونداء Joryio.unregisterPush() قبل initialize() يتغلّب على الرمز الذي يسلّمه Firebase عند التشغيل.
يعمل requestPushPermission(activity) مع أي Activity. عندما يكون نشاطك android.app.Activity عاديًا (كما في Unity)، يعرض الـ SDK مربع حوار النظام من نشاط خفي خاص به ويستدعيك عندما يجيب المستخدم. يعطي PushPermissionStatus.publicName الحالة بالاسم الذي تستخدمه كل حزم Joryio SDK: granted أو denied أو notDetermined أو unavailable.
تُسمّى قناة الإشعارات الخاصة بالـ SDK "Notifications" في إعدادات النظام. اضبط notificationChannelName في JoryioConfig لعرض اسم خاص بك ومترجم.
4. اضبط FCM في لوحة التحكم
لإرسال إشعارات Push، أدخل بيانات اعتماد Firebase في تطبيق Android في Joryio:
- انتقل إلى الإعدادات ← التطبيقات وانقر أيقونة إشعارات Push (الجرس) في صف تطبيق Android.
- انقر Upload service account JSON واختر مفتاح حساب الخدمة في Firebase، من Firebase console: Project settings ← Service accounts ← Generate New Private Key.
- انقر حفظ الإعداد.
للحصول على تعليمات مفصلة لإنشاء المفتاح، راجع الحصول على بيانات اعتماد Firebase (FCM).
رسائل In-app
تعرض حزمة SDK رسائل داخل التطبيق نيابةً عنك. بعد التهيئة تظهر الحملات المؤهلة من تلقاء نفسها، وتُقاس مرات الظهور والنقرات والإغلاق تلقائيًا - لا شيء لتوصيله.
رموز التسليم (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: الإصدار الذي بُني قبل وجود رموز التسليم لا يرسل رمزًا.
نوعان من المحتوى
تصل كل حملة بأحد شكلين من المحتوى، وتعرض حزمة SDK كلًا منهما بطريقة مختلفة:
| المحتوى | ما هو | كيف يُعرض |
|---|---|---|
| الأصلي (Native) | بيانات منظمة - عنوان ونص وصورة وأزرار | عناصر Android حقيقية، بسمة التطبيق وخطوطه ووضعه الداكن و‑TalkBack. بدون WebView. |
| HTML | HTML و‑CSS كتبها المؤلف، محفوظة كما كُتبت (يُزال فقط <base> و<meta http-equiv="refresh">) | WebView داخل الرسالة، تحت Content-Security-Policy لا يعمل فيها إلا سكربت جسر الحزمة. |
السماح برسائل HTML
رسائل HTML معطلة افتراضيًا. لا تعمل السكربتات المكتوبة في الرسالة: تحصل كل رسالة على nonce جديد في Content-Security-Policy، فلا يعمل إلا سكربت جسر الحزمة - وتبقى كتل <script> ومعالجات on* والتضمينات التي كتبها المؤلف معطلة، ويتولى الجسر الأزرار وسمات data-joryio-action ونماذج data-jry-form. لكنها تبقى web view داخل تطبيقك، لذا فإن تفعيلها قرار يتخذه فريق التطبيق - وليس شيئًا يُفعَّل من منصة تسويق:
val config = JoryioConfig(
allowHtmlJsInAppMessages = true // الافتراضي: false
)
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api",
config = config
)
تركه معطلًا لا يعطل الرسائل داخل التطبيق. تستمر الرسائل الأصلية في الظهور، لأنها بيانات يرسمها تطبيقك بعناصره الخاصة - دون أي مفسّر. تُتجاهل حملات HTML وتُسجَّل، فيعرض التطبيق الذي لم يفعّل الإعداد لا شيء بدلًا من رسالة معطوبة.
إذا كانت سياسة الأمان لديك تمنع تنفيذ HTML مكتوب خارجيًا داخل العملية، فاترك الإعداد معطلًا وأنشئ حملاتك كرسائل أصلية.
تولي العرض بنفسك
عيّن callback لرسم واجهتك الخاصة. هذا يتجاوز عرض حزمة SDK، فلن تحصل على نسختين من الرسالة:
Joryio.getInstance().setInAppMessageCallback { campaign ->
showInAppMessage(campaign) // واجهتك
}
// أبلغ عما حدث - تقيس الحزمة تلقائيًا الرسائل التي تعرضها هي فقط
Joryio.getInstance().trackInAppImpression(campaignId, "displayed")
Joryio.getInstance().trackInAppImpression(campaignId, "clicked", "cta") // أي زر
Joryio.getInstance().trackInAppImpression(campaignId, "dismissed")
الإجراء واحد من displayed أو clicked أو dismissed أو converted؛ وأي قيمة أخرى تُسجَّل في السجل ولا تُرسل. فقط displayed يُحتسب ضمن حد التكرار للحملة - النقرة أو الإغلاق لا يستهلكان مرة عرض إضافية. يسمّي الوسيط الثالث، elementId، الزر أو الرابط ويُرسل منفصلًا عن الإجراء.
منذ الإصدار 1.3.2. إذا عيّنت callback بعد initialize()، فقد تنتهي المزامنة الأولى قبله فترسم الحزمة تلك الرسالة بنفسها. لمنع ذلك، مرّر JoryioConfig(holdInAppMessagesForHandler = true): الرسالة الجاهزة قبل تعيين callback تُحتجز - لا تُرسم، ولا يُبلغ عنها، ولا تُحتسب ضمن أي حد للتكرار - وتُسلَّم إلى callback لحظة تعيينه. أما استدعاء clearInAppMessageCallback() بدلًا من ذلك فينهي الاحتجاز وترسم الحزمة الرسالة المحتجزة. يسري الاحتجاز فقط حتى أول تعيين أو إزالة.
عرض الرسائل بنفسك
تتولى الحزمة رسم الرسائل الأصلية، لكن يمكنك تولّي الأمر بالكامل، وهو ما يقابل custom view factory:
Joryio.getInstance().setInAppMessageCallback { campaign ->
// ارسمها كما تشاء
}
ضبط callback يتجاوز المُصيِّر المدمج بدل أن يعمل بجانبه، فتظهر الرسالة مرة واحدة لا مرتين.
الحقل campaign.content جاهز بالفعل: يُصيَّر Liquid في الخادم وتصل الحقول الأصلية
نصًا عاديًا (لا تمررها إلى WebView؛ فهي بلا HTML-escape تحديدًا لأنها معدّة
لعناصر النص). سجِّل ما تعرضه عبر
trackInAppImpression(campaignId, "displayed" | "clicked" | "dismissed" | "converted")
لتبقى التحليلات متطابقة.
أنواع الرسائل
تدعم الحزمة 5 أنواع من الرسائل:
- Modal - وسط الشاشة مع خلفية معتمة
- Banner - أعلى الشاشة
- Slide-Up - إشعار صغير من الأسفل
- Full-Screen - رسالة تملأ الشاشة
- Custom - التطبيق يحدد الموضع
متى تُحتسب الرسالة معروضة
تُحتسب الرسالة معروضة - أي يُرسَل in_app.displayed ويُستهلك حد التكرار للحملة - فقط عندما يكون الشخص قد تمكّن فعلًا من رؤيتها. المدة التي يجب أن تبقى فيها على الشاشة هي IN_APP_DISPLAY_DWELL_MS (1000 مللي ثانية)، وهو الثابت نفسه في كل SDK من Joryio.
- لا يعرض الـ SDK رسالة إلا على الـ Activity النشط (resumed)، وفقط إذا لم يكن قيد الإغلاق أو مُدمَّرًا أو يُعاد إنشاؤه (تدوير الشاشة). وإلا تُحفظ الرسالة للشاشة التالية.
- تُحتسب الرسالة معروضة بعد بقائها على الشاشة ثانية واحدة والـ Activity ما زال نشطًا. إذا توقف الـ Activity مؤقتًا قبل ذلك، تبدأ الثانية من جديد عند استئنافه.
- إذا أُغلقت الشاشة قبل ذلك - إعادة توجيه بعد تسجيل الدخول، تسجيل الخروج - لا يُرسَل شيء ولا يُستهلك الحد، وتُقدَّم الرسالة في الشاشة التالية.
- النقر أو الإغلاق قبل ذلك يُحتسب مشاهدة: يُرسَل
displayedأولًا ثم النقر أو الإغلاق. إعادة الضبط أو تغيير المستخدم أو إلغاء الموافقة قبل ذلك لا يُرسل شيئًا. - إذا كنت تعرض الرسائل بنفسك: أرسل
displayedوفق القاعدة نفسها - بعد أن تبقى الرسالة ظاهرة لمدةIN_APP_DISPLAY_DWELL_MS، أو مباشرة قبل نقر أو إغلاق يأتي أبكر. يرسلInAppMessagePresenterالمخصصInAppMessagePresenter.EVENT_HIDDENعندما يزيل رسالة دون أن يتفاعل معها الشخص.
خيارات الإعداد
خصص سلوك SDK باستخدام JoryioConfig:
val config = JoryioConfig(
// Initial user ID (optional)
userId = "user-123",
anonymousId = null, // Your own anonymous id; generated if null
// Event batching
batchSize = 50, // Events per batch (at most 500)
flushInterval = 5000, // Flush interval in ms (5s); never below 1000
sendImmediately = false, // Flush after every track, no batching
maxQueueSize = 1000, // Most events kept on the device; the oldest go first
// Session management
sessionTimeout = 1800000, // Session timeout in ms (30 min), background included
trackSessionStart = true, // Auto-track session start
// Network retry
maxRetries = 3, // Immediate retries of one request before backing off
retryBackoffMs = 1000, // Base backoff between retries (ms)
requestTimeout = 10000, // Per-request HTTP timeout (ms)
// SDK authentication (optional, JWT)
enableSdkAuthentication = false,
sdkAuthenticationToken = null,
// Privacy controls
optOut = false, // Start opted out (remembered, like optOut())
trackingConsent = null, // null = not set; see Tracking Consent below
// Push: ask for notification permission at the first foreground
requestPushPermissionAtLaunch = false,
// In-app messaging
// Allow HTML in-app messages, shown in a WebView. Scripts in them do not
// run; the SDK's bridge handles buttons and forms. Native messages always
// display regardless of this setting.
allowHtmlJsInAppMessages = false,
holdInAppMessagesForHandler = false, // since 1.3.2; true: your app draws messages and sets its callback after initialize()
// Push: the channel name users see in system settings (Android 8+)
notificationChannelName = "Notifications",
// Debugging
enableDebug = false, // Enable debug logging
logLevel = null // null (default): follows enableDebug. Set to pin a level.
)
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api",
config = config
)
موافقة التتبع
enum class TrackingConsent {
GRANTED, // Full tracking allowed
PENDING, // Waiting for user decision
DENIED // User denied tracking
}
الموافقة وإلغاء الاشتراك في التتبع مفتاحان منفصلان، وهما كذلك في كل SDK من Joryio:
| المفتاح | من يضبطه | النداءات |
|---|---|---|
| موافقة التتبع | شريط الموافقة في تطبيقك | updateConsent(...)، وtrackingConsent في الإعداد |
| إلغاء الاشتراك (opt-out) | اختيار الشخص نفسه | optOut() / optIn()، وoptOut في الإعداد |
لا يجمع الـ SDK البيانات إلا عندما تكون الموافقة GRANTED ولم يلغِ الشخص اشتراكه - وأي "لا" هي التي تسود. الموافقة لا ترفع إلغاء الاشتراك أبدًا (وحده optIn() يفعل ذلك)، وoptOut() لا يغيّر الموافقة المحفوظة أبدًا.
Joryio.updateConsent(TrackingConsent.GRANTED) // granted: everything held is sent, in order
Joryio.updateConsent(TrackingConsent.DENIED) // denied: everything held is discarded
Joryio.updateConsent(TrackingConsent.PENDING) // pending: hold everything on the device
PENDINGيُبقي الأحداث والسمات على الجهاز (حتىmaxQueueSize) ولا يرسل شيئًا - لا رسائل in-app ولا تسجيل Push - إلى أن يُطلقهاGRANTEDبالترتيب، أو يتخلص منهاDENIED. ينطبق على هذا التشغيل فقط، ولا يتجاوز أبدًاDENIEDمحفوظًا. والنقر على الإشعار يُحتجز كذلك ويُبلَّغ عنه عندGRANTED.DENIEDلا يجمع شيئًا ويتخلص مما هو محتجز. وهو محفوظ على الجهاز: يبقى ساريًا عبر إعادات التشغيل إلى أن يُستدعىupdateConsent(TrackingConsent.GRANTED)أو يُضبط صراحةًtrackingConsent = TrackingConsent.GRANTEDفي الإعداد. ولا يُلغي اشتراك الشخص - يبقىisUserOptedOut()بقيمةfalse.- غير مضبوط (
trackingConsent = null، وهو الافتراضي): يبقىDENIEDالمحفوظ ساريًا إن وُجد؛ وإلا يتصرف الـ SDK كما لو كانت القيمةGRANTED، وهذا هو الصحيح لتطبيق لا يعرض شريط موافقة.
اقرأ القيمة الحالية باستخدام getTrackingConsent().
مستويات السجل
enum class LogLevel {
VERBOSE, // All logs
DEBUG, // Debug and above
INFO, // Info and above
WARN, // Warnings and errors
ERROR // Errors only
}
القيمة الافتراضية لـ logLevel هي null، أي "اتبع enableDebug": DEBUG عندما يكون التصحيح مفعّلًا وERROR عندما يكون معطّلًا. اضبطه صراحةً فقط لتثبيت مستوى - LogLevel.ERROR الصريح مع enableDebug = true يبقي الـ SDK صامتًا.
خصائص متقدمة
إدارة الجلسة
تتبع الجلسات تفاعل المستخدم تلقائيًا:
// Sessions are managed automatically with 30-minute timeout
// Get current session ID
val sessionId = Joryio.getInstance().getSessionId()
// Sessions refresh on user activity
الوقت الذي يقضيه التطبيق في الخلفية يُحتسب ضمن المهلة: التطبيق الذي يعود إلى الواجهة بعد أكثر من sessionTimeout يبدأ جلسة جديدة ويرسل Session Start جديدًا.
سمات المستخدم
// Set multiple attributes
Joryio.getInstance().setAttributes(mapOf(
"age" to 28,
"city" to "San Francisco",
"premium" to true
))
// Set single attribute
Joryio.setAttribute("language", "en")
// Increment numeric attribute
Joryio.incrementAttribute("page_views", 1)
Joryio.incrementAttribute("total_spent", 29.99)
// Remove attribute
Joryio.getInstance().unsetAttribute("temporary_flag")
عناصر تحكم الخصوصية
// أوقف الجمع. يُحفظ - ويبقى بعد إعادة تشغيل التطبيق.
Joryio.optOut()
// العودة إلى التتبع - النداء الوحيد الذي يرفع إلغاء الاشتراك
Joryio.optIn()
// مفتاح إلغاء الاشتراك وحده؛ أما الموافقة فهي getTrackingConsent()
if (Joryio.isUserOptedOut()) {
Log.i("MyApp", "User has opted out")
}
// احذف كل ما خزّنه الـ SDK على هذا الجهاز.
// منفصل عن optOut(): "أوقف الجمع" و"احذف ما لديك" طلبان مختلفان.
// هذا هو النداء الذي يحتاجه طلب المحو. وهو لا يُلغي اشتراك المستخدم في
// التتبع - استدعِ optOut() أيضًا إن كان ذلك مقصودًا كذلك.
Joryio.wipeData()
// احصل على معلومات الهوية
val (userId, anonymousId) = Joryio.getInstance().getIdentity()
println("User: ${userId ?: "anonymous"}, Anonymous ID: $anonymousId")
ما يفعله optOut() بالضبط:
| يوقف الجمع | نعم - كل عملية كتابة تصبح بلا أثر: الأحداث، وidentify، والسمات (بما فيها الزيادات والإزالات)، والأسماء البديلة، وطلبات الاشتراك، وتسجيل Push، وإيصالات التسليم وتتبع النقرات، وتقارير in-app. أما إلغاءات الاشتراك وإلغاء تسجيل Push فتُرسل مع ذلك |
| يبقى بعد إعادة التشغيل | نعم - تُحفظ العلامة على الجهاز وتُقرأ قبل جمع أي شيء |
| يتخلص مما في الطابور | نعم - تُحذف الأحداث المنتظرة وكتابات السمات غير المؤكدة، ولا تُرسل لاحقًا. أما إلغاء الاشتراك الذي لم يُسلَّم بعد فيُحفظ ويُرسل |
| يُبلغ الخادم | نعم - سمة الملف الشخصي tracking_opted_out، تُرسل عندما يتغير المفتاح (بما في ذلك إلغاء الاشتراك المقرر عند التهيئة أو قبلها) ويُعاد إرسالها إلى أن يتسلمها الخادم، أيًا كانت الموافقة بالنسبة إلى true (أما false الذي يرسله optIn() فينتظر الموافقة GRANTED) |
| يحذف البيانات المخزنة | لا - استخدم wipeData() |
السمة tracking_opted_out سجلّ وليست أداة إنفاذ: فهي تُكتب في الملف الشخصي
لكي تتمكن الحملات من الاستبعاد بناءً عليها. أما الحجب من جهة الخادم فهو إعداد
منفصل.
تفريغ الطابور يدويًا
// Flush events immediately
Joryio.flush()
// Useful before app termination
override fun onDestroy() {
super.onDestroy()
Joryio.flush()
}
أفضل الممارسات
1. هيئ مبكرًا
هيئ SDK في فئة Application:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api"
)
}
}
2. تتبع مشاهدات الشاشة
استخدم تتبع الشاشات للتنقل:
override fun onResume() {
super.onResume()
Joryio.trackScreen(this::class.simpleName ?: "Unknown")
}
3. تعامل مع تسجيل خروج المستخدم
أعد الضبط دائمًا عند تسجيل الخروج:
fun logout() {
// Clear user session
clearUserSession()
// Reset SDK
Joryio.reset()
}
الإرسال والموافقة ودورة حياة البيانات
تُحفظ عمليات الكتابة إلى أن يؤكدها الخادم. تُحفظ identify وaddAlias وincrementAttribute وunsetAttribute وsetSubscription ونداءات مجموعات الاشتراك في صندوق صادر مشفّر على الجهاز، وتُرسل بالترتيب الذي تمت به. عند الفشل - عدم الاتصال، أو انتهاء المهلة، أو 5xx، أو 429، أو 200 يجيب بـ success: false - تبقى الكتابة ويُعاد إرسالها مع انتظار متزايد، حتى بعد إعادة التشغيل. يُحترم Retry-After بالثواني أو كتاريخ، وحتى flush() ينتظر انقضاءه. أما الكتابة التي لن يقبلها الخادم أبدًا - 4xx مثل 400 أو 409 أو 413 أو 422، أو 403 ناتج عن سياسة الهوية لديك - فتُحذف مع سطر في السجل، حتى لا تعطّل ما بعدها. ولا يُحتسب على الكتابة أبدًا عدمُ الاتصال أو انتهاء المهلة أو 429 أو 5xx. والكتابة التي تتلقى 401 أو 200 مع success: false يُعاد إرسالها عشر مرات على الأكثر، والتي تتلقى 404 (لا ملف شخصي بعد) خمس مرات على الأكثر: تغيير الاشتراك الذي يُجاب بـ 404 يُعاد إرساله بعد إرسال الأحداث المنتظرة، لأنها هي التي تنشئ الملف الشخصي. وللقيم التي يرسلها setAttributes الحد نفسه من عشر إجابات. ونداءات السمات على المفتاح نفسه تُطبَّق بالترتيب الذي أجريتها به، أيًا كان ما استخدمته من setAttribute وunsetAttribute وincrementAttribute. لـ setAttributes طابور دائم خاص به، موصوف أعلاه؛ وتُحذف هناك أيضًا السمات التي يرفضها الخادم لأنها غير صالحة (400).
حدود الأحداث. يرفض الخادم الحدث الذي تزيد خصائصه على 50 KB، أو تحتوي على أكثر من 200 مفتاح، أو يتجاوز تداخلها 5 مستويات، أو يكون اسمه فارغًا أو أطول من 255 حرفًا. يتحقق track() من ذلك على الجهاز، ويقيس الحجم كما يقيسه الخادم (حرف مثل < أو & يُحتسب مرة واحدة)، ويحذف مثل هذا الحدث مع سطر في السجل، بدلًا من وضع شيء في الطابور لا يمكن تسليمه أبدًا. والاسم المكوّن من مسافات فقط اسم صالح. والحدث الذي يظل الخادم يرفضه نهائيًا يُحذف ولا يُعاد إرساله.
القيمة null تحذف السمة. setAttribute("plan", null) مماثل لـ unsetAttribute("plan")، في كل SDK من Joryio.
الجلسات. تنتهي الجلسة بعد sessionTimeout دون نشاط، سواء كان التطبيق في الخلفية أو بقي مفتوحًا على شاشة واحدة. أول حدث بعد ذلك يبدأ جلسة جديدة ويرسل Session Start الخاص بها أولًا. وreset() يبدأ جلسة جديدة كذلك. والتشغيل الذي يحدث خلال sessionTimeout من آخر نشاط يواصل تلك الجلسة ولا يرسل Session Start جديدًا. أما الجلسة التي بدأت بينما كان التتبع محظورًا (إلغاء التتبع أو رفض الموافقة) فلا تُواصَل بهذه الطريقة: أول تشغيل يُسمح له بالتتبع يبدأ جلسة جديدة مع Session Start الخاص بها.
الموافقة عند التشغيل. optOut = true يعمل مثل optOut(): يُحفظ على الجهاز، فيبقى التشغيل اللاحق خارج التتبع حتى optIn(). وtrackingConsent = TrackingConsent.DENIED يُحفظ كذلك، بوصفه موافقة (انظر موافقة التتبع). وفي الحالتين يُحذف كل ما تركه تشغيل سابق في الطابور. ونداءات Joryio.optOut() وJoryio.optIn() وJoryio.updateConsent() وJoryio.wipeData() قبل initialize() يطبّقها initialize() نفسه، قبل جمع أي شيء أو إرساله - وتُحفظ على الجهاز حتى ذلك الحين، فيبقى القرار الذي اتُّخذ قبل بدء الـ SDK قائمًا حتى لو أُغلق التطبيق. وقبل initialize()، يجيب Joryio.isUserOptedOut() وJoryio.getTrackingConsent() بما هو محفوظ على الجهاز - إلغاء تتبع أو رفض من تشغيل سابق - لا بقيمة افتراضية. ونداء Joryio.updateConsent(TrackingConsent.PENDING) قبل initialize() يسري على ذلك التشغيل وحده ولا يعيد فتح الرفض أبدًا، ونداء Joryio.optIn() أو Joryio.optOut() قبل initialize() يتغلّب على optOut في الإعداد. وعندما لا يكون أي شيء محفوظًا، يجيب Joryio.getTrackingConsent() بـ GRANTED، كما في iOS.
مصادقة SDK. عند تفعيل enableSdkAuthentication، يُستدعى المعالج المضبوط عبر setSdkAuthenticationErrorHandler عند كل 401 خاص بالمصادقة، بما في ذلك عندما لا يكون أي رمز قد ضُبط بعد (السبب MISSING): قدّم رمزًا باستخدام setSdkAuthenticationToken. تُرسل الأحداث في دفعات لمستخدم واحد في كل مرة؛ وأحداث أو كتابات مستخدم سجّل خروجه منذ ذلك الحين، والتي يرفضها الخادم لأي سبب من أسباب المصادقة، تُحذف، فلا تؤخر أبدًا أحداث المستخدم التالي. ولا يُستدعى المعالج إلا لطلبات المستخدم المسجّل دخوله الآن، وليس أبدًا لبقايا مستخدم سجّل خروجه. وreset() يُخرج الرمز من الجلسة، ويبقى مع ما تركه المستخدم المغادر في قائمة الانتظار. يحمل كل طلب الرمز الذي يكون sub فيه هو مستخدم ذلك الطلب - وليس أبدًا رمزًا يسمّي شخصًا آخر - ولا يحمل طلب الزائر المجهول أي رمز. والرمز المضبوط للمستخدم التالي قبل reset()، حتى عندما لا يكون أحد مسجّلًا دخوله، يُحفظ له ويُستخدم من جديد عند identify() الخاص به؛ وwipeData() يحذفه. وإلا فاضبط رمز المستخدم التالي بـ setSdkAuthenticationToken بعد تسجيل دخوله. أما الأحداث وعمليات الكتابة التي وضعها المستخدم المغادر في قائمة الانتظار فتُرسل مع رمزه هو، فلا يضيع شيء عند تسجيل الخروج العادي.
قبل التهيئة. النداءات على Joryio قبل انتهاء initialize() تُحفظ - حتى 1000، ويُحذف الأقدم أولًا - وتُنفَّذ بالترتيب. والحدث أو كتابة السمة أو الاسم المستعار (alias) أو الاشتراك الذي يُجرى من خلالها بينما ألغى المستخدم التتبع (opt-out) أو رُفضت الموافقة - بقرار قبل initialize() أو محفوظ من تشغيل سابق - يُحذف فورًا، فلا يرسله أبدًا optIn() أو منح موافقة لاحق؛ أما إلغاءات الاشتراك وidentify() وreset() فتُحفظ. وoptOut() أو updateConsent(DENIED) قبل initialize() يحذف أيضًا الأحداث وكتابات السمات والأسماء المستعارة والاشتراكات التي دخلت قائمة الانتظار قبله، كما يحدث بعد التهيئة، فلا يرسلها أبدًا optIn() أو منح موافقة لاحق قبل initialize(). والرمز (token) الذي يُعيَّن بـ Joryio.setSdkAuthenticationToken() قبل أي identify() أو reset() أو wipeData() تطبّقه initialize() نفسها، قبل أول طلب لها. اضبط الرمز والمعالج بالدالتين الثابتتين Joryio.setSdkAuthenticationToken() وJoryio.setSdkAuthenticationErrorHandler()، وليس داخل whenReady: فهما وحدهما تؤخذان قبل أول طلب وتبقيان بعد wipeData() يُجرى قبل initialize()، فتظل إلغاءات اشتراك المستخدم الممسوح تُرسل مع رمزه. Joryio.getInstance() هو النداء الوحيد الذي يرمي استثناءً (IllegalStateException) قبل initialize()؛ وحيث قد لا تكون التهيئة قد انتهت، استخدم الدوال الثابتة أو Joryio.whenReady { sdk -> ... }.
حساب موقوف مؤقتًا (402). يعني 402 أن استقبال البيانات في حسابك موقوف مؤقتًا. لا يُحتسب أبدًا ضمن حدّ إعادة محاولات الكتابة، ولا يؤخّر عمليات السحب: يُرسل إلغاء الاشتراك، والخروج من مجموعة، وسجل tracking_opted_out = true المنتظِرة خلف كتابة مرفوضة، بينما تنتظر الكتابات الأخرى ويُعاد إرسالها مع انتظار متزايد.
تسجيل الخروج وتبديل الحساب. يُحفظ رمز مصادقة SDK الخاص بالمستخدم المغادر في المخزن المشفّر مع أحداثه وكتاباته المنتظرة، فتُسلَّم به حتى لو أُغلق التطبيق قبل إرسالها؛ ويُحذف بعد تسليمها. يُعامَل identify() لمستخدم آخر من دون reset() بالطريقة نفسها بالنسبة إلى المستخدم السابق: تُرسل سماته غير المرسلة بمعرّفاته هو، ويذهب الرمز مع ما تبقّى له - إلا إذا كنت قد ضبطت رمز المستخدم الجديد، الذي يتعرّف عليه SDK من حقل sub. لا يُستبدل إلغاء اشتراك منتظِر إلا بتغيير أحدث للمستخدم نفسه.
identify() أثناء إلغاء التتبع أو رفض الموافقة. يُحفظ معرّف المستخدم على الجهاز فقط - لا يُرسل شيء ولا يُكتب أي ملف شخصي - كي يحمله إلغاء اشتراك يتم في هذه الأثناء. عندما يُسمح بالتتبع من جديد (optIn()، updateConsent(TrackingConsent.GRANTED))، يعمل identify() كالمعتاد. ويُنسى عند reset() وwipeData(). ويحدث ذلك أيضًا في تشغيل لاحق تسمح حالته المحفوظة أو إعداده بالتتبع. ونداء identify() جديد يحل محل معرّف المستخدم المحتجز، وuserId في الإعداد يتغلّب عليه. ويحمله كذلك سجل إلغاء التتبع وإلغاء تسجيل Push اللذان يتمان في هذه الأثناء، ومع SDK Authentication يطلب رفضهما من الـ handler الخاص بك رمز ذلك المستخدم.
أرقام لا يستطيع JSON حملها. تُحذف قيمة NaN أو Infinity في قيمة سمة أو في خاصية حدث أو تجارة إلكترونية - هذا المفتاح فقط - مع سطر في السجل، ويستمر باقي النداء. ويُتخطّى سطر null في قائمة items للتجارة الإلكترونية.
قراءة الجلسة لا تغيّر شيئًا. لا يبدأ getSessionId() وgetSessionInfo() جلسة ولا يمدّدانها أبدًا. الأحداث وعودة التطبيق إلى الواجهة وحدها تفعل ذلك، وترسل الجلسة الجديدة دائمًا Session Start الخاص بها.
امتلاء المخزن المؤقت قبل التهيئة. عندما يمتلئ مخزن الـ 1000 نداء، يُحذف أولًا أقدم نداء ليس إلغاء اشتراك أو خروجًا من مجموعة أو Joryio.unregisterPush().
المفاتيح والتخزين والخيوط. يُرفض مفتاح SDK صادر لمنصة أخرى (jry_sdk_ios_...، jry_sdk_web_...، متجر) مع خطأ، ولا يرسل SDK شيئًا. يُعاد تجربة خطأ Keystore ولا يحذف المخزن المشفّر أبدًا؛ لا يُعاد إنشاء إلا المخزن الذي لا يمكن فك تشفيره (استعادة من نسخة احتياطية)، والتشغيل الذي جرى أثناء عدم توفر Keystore يبدأ من ملف عادي من دون الهوية المحفوظة؛ وفي التشغيل التالي الذي يفتح المخزن المشفّر يتغلّب آخر تغيير أجراه على كل قيمة - بما في ذلك تسجيل الخروج أو wipeData() أو optIn() أو منح الموافقة في ذلك الوقت - بينما لا يحل المعرّف المجهول ومعرّف الجهاز اللذان اضطر إلى إنشائهما محل المحفوظين، وتُحفظ إلغاءات الاشتراك والكتابات المنتظرة من التشغيلين. وإذا سجّل ذلك التشغيل الخروج أو أدخل مستخدمًا آخر، فلا يُنقل المعرّف المجهول للمستخدم المحفوظ، ولا تُرسل سماته غير المرسلة أبدًا باسم المستخدم الجديد - تُحذف بعد تسجيل الخروج، وإلا فتُرسل بمعرّفاته هو. والمفتاح الرئيسي الذي يبقى متعذر الاستخدام - في ثلاثة تشغيلات متتالية لم يُفتح فيها أي مخزن به، مع سبب يُظهر أنه لا يستطيع فك التشفير أو مع إخفاقات تستمر يومًا على الأقل - يُستبدل بمفتاح جديد خاص بـ SDK (لا يُمس مفتاح androidx الخاص بتطبيقك أبدًا). عندئذ يُتخلّص من المخزن المشفّر بالمفتاح القديم، ويضيع كل ما بقي فيه ولم تكتبه من جديد التشغيلاتُ التي جرت بالملف العادي - بما في ذلك إلغاءات الاشتراك المنتظرة؛ أما المخزن الذي لا يزال المفتاح القديم يفتحه فيُنقل أولًا إلى المفتاح الجديد. ولا ينتظر الخيط الرئيسي (main thread) إعادة المحاولة أبدًا. مع initializeAsync()، يُلتقط Activity فُتح أثناء بدء SDK - لرسائل in-app، وطلب الإذن عند التشغيل، والنقر على الإشعار الذي فتحه. يعمل callback رسائل in-app الخاص بك على الخيط الرئيسي (main thread)، ويصل نوع الرسالة الذي لا يعرفه هذا الإصدار من SDK على أنه MessageType.CUSTOM.
رسائل HTML تبقى في مستندها. لا يُحمَّل إلا مستند الرسالة نفسه. يُلغى أي تنقّل تبدؤه - رابط أو تحديث أو frame - ويُفتح الرابط الذي ينقر عليه المستخدم في المتصفح. ولا يستطيع CSS الخاص بها إغلاق عنصر <style> الخاص به.
unregisterPush() يدوم. يُنسى الرمز على الجهاز فورًا ويُحفظ الطلب إلى أن يؤكده الخادم. لا يسجّل SDK رمزًا من تلقاء نفسه مرة أخرى - لا في التشغيل التالي ولا عندما يغيّر Firebase الرمز - إلى أن تستدعي registerPushToken() أو يمنح المستخدم الإذن عبر requestPushPermission().
wipeData() يمحو المعرّف المجهول ومعرّف المستخدم وطابور الأحداث والكتابات المعلّقة وحالة In-app وسجل مرات العرض والجلسة ومعرّف الجهاز ورمز Push - ويلغي تسجيل ذلك الرمز على الخادم، تحت الهوية التي تُمحى. ويُبقي على إلغاء الاشتراك في التتبع إن وُجد: حذف البيانات ليس موافقة.
بعد ذلك تبقى الإشعارات متوقفة حتى يستدعي التطبيق registerPushToken مجددًا أو يطلب الإذن من جديد - ولا يُسجَّل تلقائيًا رمز يصل في تشغيل لاحق.
عرض واحد لكل مُحفِّز. تُحجز الحملة لحظة اختيارها، قبل أي تأخير في العرض، فإذا وقع مُحفِّزان متتاليان بسرعة (الحدث نفسه يُتتبَّع مرتين، أو مزامنة تصل بينما تنتظر رسالة مؤجلة) تُعرض مرة واحدة. ينتهي الحجز عند إغلاق الرسالة (حتى عندما لا يجوز إرسال أي شيء) أو عندما يتعذر عرضها. ولا تُعرض أي رسالة ما دامت الموافقة ليست GRANTED أو كان الشخص قد ألغى التتبع. والمزامنة التي كانت جارية بالفعل عند حدوث identify() أو reset() أو wipeData() أو تغيير في الموافقة تُهمَل، وتليها مزامنة جديدة.
تكرار In-app. فقط تقرير displayed يستهلك حد التكرار للحملة والفاصل قبل الحملة التالية. العارض المدمج في SDK يبلّغ عنه عند رسم الرسالة؛ ومع setInAppMessageCallback تبلّغ عنه أنت بـ trackInAppImpression(campaignId, "displayed"). تسليم رسالة إلى الـ callback الخاص بك لا يستهلك شيئًا، فالرسالة التي تقرر عدم عرضها لا تؤخر التالية. clearInAppMessageCallback() يعيد العرض إلى SDK. والـ presenter المثبّت بـ setInAppPresenter يبلّغ عبر الـ callback onEvent الذي يتلقاه ("displayed" و"clicked" و"dismissed") ولا يجوز أن يستدعي trackInAppImpression أيضًا، وإلا احتُسبت الرسالة مرتين.
سجل إلغاء التتبع. يكتب optOut() سمة الملف الشخصي tracking_opted_out = true (دون البادئة $)، ويعيدها optIn() إلى false - وكلٌّ منهما لا يفعل ذلك إلا عندما يتغير المفتاح فعلًا. والسجل الذي لا يزال منتظرًا عندما يسجّل مستخدمه الخروج يبقى خاصًا به ويُسلَّم إلى ملفه الشخصي، ولا يُوجَّه أبدًا إلى الشخص التالي.
مرجع API
كل الدوال العامة، بأسماء معاملاتها. النداءات بالشكل Joryio.x() ثابتة وآمنة قبل التهيئة؛ أما النداءات عبر Joryio.getInstance() فتحتاج إلى SDK مهيأ.
// Setup
Joryio.initialize(context: Context, sdkKey: String?, apiHost: String?, config: JoryioConfig? = JoryioConfig()): Joryio
Joryio.initializeAsync(context: Context, sdkKey: String?, apiHost: String?, config: JoryioConfig? = JoryioConfig(), onReady: ((Joryio) -> Unit)? = null)
Joryio.isInitialized(): Boolean
Joryio.getInstance(): Joryio // throws IllegalStateException before initialize()
Joryio.whenReady(action: ((Joryio) -> Unit)?) // runs now, or as soon as the SDK is initialized
Joryio.SDK_VERSION: String
// Events
Joryio.track(eventName: String?, properties: Map<String, Any?>? = emptyMap())
Joryio.trackScreen(screenName: String?, properties: Map<String, Any?>? = emptyMap())
Joryio.flush()
Joryio.getInstance().ecommerce(config: EcommerceConfig? = null): EcommerceTracker
// Identity and attributes
Joryio.identify(userId: String?)
Joryio.addAlias(label: String?, name: String?) // works before identify() too
Joryio.reset() // logout: forgets the user, new anonymous id
Joryio.setAttribute(key: String?, value: Any?) // a null value removes the attribute
Joryio.setAttributes(attributes: Map<String, Any?>?)
Joryio.incrementAttribute(key: String?, by: Number? = 1)
Joryio.unsetAttribute(key: String?)
Joryio.getInstance().getAnonymousId(): String
Joryio.getInstance().getUserId(): String?
Joryio.getInstance().getSessionId(): String
Joryio.getInstance().getSessionInfo(): SessionInfo
Joryio.getInstance().getIdentity(): Pair<String?, String> // (userId, anonymousId)
// Tracking consent, opt-out and stored data
Joryio.optOut()
Joryio.optIn()
Joryio.isUserOptedOut(): Boolean // the opt-out switch only
Joryio.updateConsent(consent: TrackingConsent?) // GRANTED, DENIED or PENDING
Joryio.getTrackingConsent(): TrackingConsent? // before initialize(): the stored decision, or GRANTED when there is none
Joryio.getInstance().getTrackingConsent(): TrackingConsent
Joryio.wipeData() // safe before initialize()
// Marketing subscription
Joryio.setSubscription(channel: String?, status: String?)
Joryio.addToSubscriptionGroup(groupId: String?, channel: String?)
Joryio.removeFromSubscriptionGroup(groupId: String?, channel: String?)
Joryio.unregisterPush()
// In-app messages
Joryio.getInstance().setInAppMessageCallback(callback: ((InAppCampaign) -> Unit)?, capabilities: List<String>? = null)
Joryio.clearInAppMessageCallback() // hands rendering back to the SDK
Joryio.getInstance().setInAppPresenter(presenter: InAppMessagePresenter?)
Joryio.syncInAppCampaigns()
Joryio.getInstance().trackInAppImpression(campaignId: String?, action: String?, elementId: String? = null)
Joryio.getInstance().submitInAppForm(campaignId: String?, values: Map<String, Any?>?, onResult: ((String) -> Unit)?)
Joryio.resetDisplayedCampaigns() // testing only: needs enableDebug
Joryio.getInstance().evaluateInAppCampaigns() // testing only: needs enableDebug
// Push
Joryio.getInstance().registerPushToken(token: String?, onResult: ((PushRegistrationResult) -> Unit)? = null)
Joryio.getInstance().unregisterPush()
Joryio.getInstance().isPushEnabled(): Boolean
Joryio.getInstance().getDeviceToken(): String?
Joryio.getInstance().getPushPermissionStatus(): PushPermissionStatus
Joryio.getInstance().requestPushPermission(activity: Activity?, callback: ((PushPermissionStatus) -> Unit)? = null)
Joryio.getInstance().reportPushDelivered(trackingId: String?) // only with your own messaging service
Joryio.getInstance().trackPushClick(trackingId: String?) // only when you handle the tap yourself
// SDK authentication
Joryio.setSdkAuthenticationToken(token: String?)
Joryio.setSdkAuthenticationErrorHandler(handler: ((SdkAuthError) -> Unit)?)
// Diagnostics
Joryio.getInstance().getQueueSize(): Int
Joryio.getInstance().apiHost: String
Joryio.getInstance().currentApiEndpoint: String
Joryio.getInstance().lastTransportError: JoryioTransportError?
استكشاف الأخطاء وإصلاحها
الأحداث لا تظهر
- تحقق من صحة مفتاح SDK:
jry_sdk_android_*. - فعّل سجل التصحيح:
enableDebug = true. - افحص logcat بحثًا عن أخطاء.
- تحقق من أذونات الشبكة في manifest.
- استدعِ
flush()للإرسال فورًا.
Push لا يعمل
- تحقق من ضبط Firebase بشكل صحيح.
- تحقق من رفع Firebase service account JSON في لوحة التحكم.
- تأكد من تسجيل رمز الجهاز.
- اختبر باستخدام Firebase Console أولًا.
- ابحث عن خطأ إرسال Push تجريبي في الحصول على بيانات اعتماد Firebase (FCM).
أخطاء البناء
- تأكد من أن الحد الأدنى لإصدار SDK هو 23.
- زامن تبعيات Gradle.
- نظف المشروع وأعد بناءه.
بُنى الإصدار (R8) والنسخ الاحتياطي
- R8 / ProGuard: يحمل كلا الـ artifactين قواعدهما الخاصة، وتُطبَّق تلقائيًا عندما يضبط تطبيقك
isMinifyEnabled = true(بما في ذلك وضع R8 الكامل). لا تحتاج إلى إضافة قواعد، والتطبيق الذي لا يتضمنfirebase-messagingيُبنى مع ذلك - إشعارات Push تكون معطلة فحسب. - النسخ الاحتياطي: لا يلزم أي شيء. يقع طابور الأحداث في مجلد التطبيق المستثنى من النسخ الاحتياطي، والمخزن المشفّر المستعاد من نسخة احتياطية (التي لا تتضمن أبدًا مفتاح Keystore) يُكتشف ويُعاد إنشاؤه، مشفّرًا كما كان. إذا كان تطبيقك يعلن قواعد نسخ احتياطي خاصة به وتريد استثناء ملفات الـ SDK صراحةً، فأشر إلى
@xml/joryio_backup_rules(android:fullBackupContent) و@xml/joryio_data_extraction_rules(android:dataExtractionRules)، أو انسخ أسطرهما إلى قواعدك. لا يضبط الـ SDK هذه السمات بنفسه، لذا لا يتعارض أبدًا مع ملف manifest الخاص بك.
تتبع E-Commerce
يتضمن Android SDK متتبعًا مدمجًا للتجارة الإلكترونية لأحداث المنتجات والسلة وإتمام الشراء والطلبات. راجع دليل تتبع E-Commerce العابر لـ SDKs لمعرفة API الكاملة مع أمثلة Kotlin.
عملية شراء، بالمفاتيح التي تقرؤها تقارير الإيرادات:
import io.joryio.sdk.ecommerce.EcommerceCartItem
import io.joryio.sdk.ecommerce.EcommerceOrder
Joryio.getInstance().ecommerce().purchase(
EcommerceOrder(
orderId = "ORD-1001",
items = listOf(EcommerceCartItem(productId = "SKU-1", name = "Blue Shirt", price = 29.99, quantity = 2)),
value = 59.98,
currency = "USD",
)
)
يرسل هذا Order Completed مع orderId وtotal. إذا تتبعت عملية شراء يدويًا، فاستخدم هذين المفتاحين: الطلب المرسل بـ order_id أو value يُسجَّل دون إيرادات.
عملية الشراء التي لا تحمل orderId، أو تحمل value ليس رقمًا منتهيًا، أو تتضمن عنصرًا بلا productId، تُتجاهل مع سطر في السجل ولا تُسجَّل أبدًا كطلب بقيمة صفر؛ ولا ترمي أي دالة في المتتبِّع استثناءً. ويعمل EcommerceTracker.create() قبل initialize(): تنتظر أحداثه جاهزية الـ SDK.
الخطوات التالية
- iOS SDK: دمج iOS SDK.
- Web SDK: دمج Web SDK.
- دليل إشعارات Push: تعرّف إلى حملات Push.
- دليل حملات In-app: إنشاء رسائل داخل التطبيق.
- Custom Events: تتبع الأحداث المخصصة.
- E-Commerce API: دمج التجارة الإلكترونية من الخادم.
واجهات الاختبار والتشخيص
مجموعتان، والفرق بينهما مهم.
واجهات الاختبار - تُتجاهَل ما لم يكن enableDebug مفعّلًا. فهي تغيّر حالة
حيّة، ومن ثم فإن استدعاءً عارضًا في نسخة الإنتاج سيفسد حدود التكرار والتقارير
الحقيقية.
| الدالة | ما الذي تفعله |
|---|---|
resetDisplayedCampaigns() | تنسى أي حملات in-app عُرضت بالفعل. حالة التكرار محفوظة على الجهاز، لذا فإن تغيير الحملة على الخادم لن يعيد عرضها. |
evaluateInAppCampaigns() | تعيد تنفيذ قرار العرض على الحملات المُزامنة دون طلب شبكة. |
واجهات التشخيص - متاحة دائمًا، بما في ذلك الإنتاج. فهي تقرأ الحالة فقط ولا يمكنها إحداث ضرر.
| الدالة | تجيب عن |
|---|---|
getQueueSize() | كم حدثًا ينتظر الإرسال؟ |
currentApiEndpoint / lastTransportError | مع أي خادم نتحدث، وهل فشل آخر طلب؟ |
الدوال getIdentity() وgetSessionInfo() وgetDeviceToken() ليست ضمن أي من المجموعتين - فهي واجهة عادية. قراءة هوية الجهاز أمر طبيعي للتطبيق.
تسليم السمات
setAttributes دائم. تبقى الكتابة في الطابور إلى أن يؤكد الخادم استلامها،
فالسمة التي تُضبط بينما الجهاز غير متصل تُسلَّم عند عودة الاتصال بدلًا من أن
تضيع.
- يُعاد إرسالها عند نداء
setAttributesالتالي، وعند العودة إلى الواجهة، وقبل مزامنة in-app. - تُجمَّع في دفعات - سلسلة سريعة من النداءات تصبح طلبًا واحدًا (نافذة 800ms). والكتابة المنفردة تُرسل فورًا مع ذلك.
- تُحفظ في المخزن المشفّر للمنصة (iOS Keychain، وAndroid
EncryptedSharedPreferences)، فتبقى الكتابة حتى لو أُنهيت العملية. تُمسح
لحظة تأكيد الخادم، ويحذفها
optOut()وwipeData().
تُستخدم السمات أيضًا لاستهداف in-app. الملف الشخصي على الخادم هو المرجع: وحدها الكتابات التي لم يؤكدها الخادم بعد يمكنها أن تتجاوزه، وهذا ما يمنع جهازين يخصّان جهة الاتصال نفسها من الاختلاف حول هوية جهة الاتصال هذه.
الاشتراك التسويقي
ما إذا كان مسموحًا بمراسلة هذا الشخص على قناة ما. هذه موافقة مختلفة عن optOut() التي تخص ما إذا كان مسموحًا بتتبّعه - ولا تستلزم إحداهما الأخرى.
هذا ما يكتبه مركز التفضيلات داخل التطبيق.
Joryio.setSubscription("email", "unsubscribed")
Joryio.addToSubscriptionGroup("list_abc123", "email")
القنوات: email, sms, whatsapp, push, viber. الحالات: optedIn, subscribed, unsubscribed.
المعرّف مطلوب - تُتخطّى المكالمة، مع تسجيل خطأ، إن لم يوجد معرّف مستخدم ولا معرّف مجهول.
إلغاء الاشتراك يعمل دائمًا. يُقبل setSubscription(channel, "unsubscribed") وremoveFromSubscriptionGroup ويُرسَلان حتى عندما يكون الشخص قد ألغى التتبع، أو تكون الموافقة DENIED أو PENDING، وبعد wipeData() - فطلب إيقاف الرسائل لا يُؤخَّر ولا يُحذف أبدًا. أما الاشتراك (subscribed وoptedIn وaddToSubscriptionGroup) فيُرفض أثناء إلغاء التتبع أو الرفض، ويُحتجز ما دامت الموافقة معلّقة. والتغيير الأحدث للقناة نفسها أو المجموعة نفسها يحل محل تغيير أقدم لا يزال ينتظر الإرسال، والتغيير الذي يتم قبل identify() ويُرسل بعده يحمل معرّف المستخدم أيضًا.