تخطّ إلى المحتوى الرئيسي

دمج iOS SDK

SDK أصلي لـ iOS لتتبع الأحداث وإدارة جلسات المستخدمين وإرسال إشعارات Push وعرض رسائل In-app.

الخصائص​

  • خفيف: أثر محدود.
  • سريع: محسّن للأداء.
  • دعم عدم الاتصال: طابور أحداث مبني على SQLite.
  • إعادة محاولة تلقائية: تراجع أسي عند الإخفاق.
  • دفعات: تجميع فعال للأحداث، 50 حدثًا/5 ثوانٍ.
  • رسائل In-app: رسائل أصلية و‑HTML، 5 أنواع، مع حدود تكرار.
  • إشعارات Push: Apple Push Notification Service، APNS.
  • الخصوصية أولًا: متوافق مع GDPR ويحترم موافقة المستخدم.
  • iOS 14+: دعم إصدارات iOS الحديثة.

المتطلبات​

  • iOS 14.0+.
  • Xcode 15.0+.
  • Swift 5.9+.
الاستدعاء من Objective-C

واجهة الـ SDK مكتوبة بلغة Swift فقط، لذلك لا يمكن استدعاؤها مباشرة من ملف .m أو .mm. يدمجها تطبيق Objective-C بإضافة ملف Swift واحد يستورد Joryio ويكشف الاستدعاءات التي يحتاجها التطبيق، ثم يستدعيه من Objective-C كالمعتاد. يقوم Xcode بإعداد الـ bridging header تلقائيًا عند إضافة أول ملف Swift إلى هدف Objective-C.

التثبيت​

Swift Package Manager (موصى به)​

يُوزَّع SDK كحزمة Swift من https://github.com/joryio/joryio-ios-sdk. هذه هي طريقة التثبيت الموصى بها؛ أما CocoaPods فقد أُوقف دعمه (راجع CocoaPods). أضف التالي إلى Package.swift:

dependencies: [
.package(url: "https://github.com/joryio/joryio-ios-sdk.git", from: "1.3.3")
]

توفّر الحزمة ثلاثة منتجات (products). اربط الأول والثاني بتطبيقك حسب الحاجة؛ أما الثالث فهو مخصص فقط لـ Notification Service Extension (راجع صور إشعارات Push):

.target(
name: "YourApp",
dependencies: [
.product(name: "Joryio", package: "joryio-ios-sdk"), // التتبع والهوية والإشعارات
.product(name: "JoryioUI", package: "joryio-ios-sdk"), // + عرض الرسائل داخل التطبيق
]
)

أو في Xcode:

  1. File ← Add Package Dependencies.
  2. أدخل: https://github.com/joryio/joryio-ios-sdk.git.
  3. اختر الإصدار وأضفه إلى target.

CocoaPods (متوقف الدعم)​

متوقف الدعم - استخدم Swift Package Manager

أُوقف دعم CocoaPods لـ SDK الخاص بـ Joryio على iOS لصالح Swift Package Manager. يُغلَق CocoaPods trunk، وهو المستودع الذي ينزّل منه سطر pod 'Joryio' العادي (للقراءة فقط بدءاً من 2 ديسمبر 2026)، لذلك لا يُضمن وجود الإصدارات الجديدة عليه. إذا كان على تطبيقك البقاء على CocoaPods، فثبّت من وسم git الخاص بالـ SDK كما في المثال أدناه: لا يمر ذلك عبر trunk، ويدعمه كل إصدار بدءاً من 1.3.0، بما في ذلك الإصدارات التجريبية. ثبّت الإصدار بـ :tag وغيّر الوسم للترقية - فالأمر pod update لا يحرّكه.

للانتقال إلى Swift Package Manager: احذف أسطر pod 'Joryio...' من Podfile، وشغّل pod install، ثم أضف الحزمة كما هو موضح أعلاه. لا يتغير شيء آخر في شيفرتك - باستثناء أن Notification Service Extension يكتب import JoryioNotificationServiceExtension بدلاً من import Joryio.

pod 'Joryio/UI', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3' # كل شيء، بما في ذلك العرض داخل التطبيق
# pod 'Joryio', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3' # التتبع والهوية والإشعارات فقط - دون ربط WebKit

# فقط في هدف Notification Service Extension:
# pod 'Joryio/NotificationServiceExtension', :git => 'https://github.com/joryio/joryio-ios-sdk.git', :tag => '1.3.3'
أيهما أختار؟

يعتمد JoryioUI على Joryio، لذا فإن ربط منتج الواجهة يمنحك كليهما.

خذ Joryio وحده إذا كان تطبيقك لا يحتاج الرسائل داخل التطبيق، أو إذا اعترضت مراجعة أمنية على ربط web view. لاحظ أن الإعداد الافتراضي في CocoaPods هو Joryio الخالي من الواجهة، فلن يعرض pod 'Joryio' رسائل داخل التطبيق حتى تبدّله بـ pod 'Joryio/UI'.

بيان الخصوصية​

يأتي SDK مع بيان الخصوصية من Apple ‏(PrivacyInfo.xcprivacy) في حزمة Swift وفي pod الخاص بـ CocoaPods. يصرّح البيان بالواجهة الوحيدة من نوع required-reason التي يستخدمها SDK - UserDefaults، السبب CA92.1 - وبالبيانات التي يجمعها SDK: معرّف المستخدم ومعرّف الجهاز والتفاعل مع المنتج وسجل المشتريات، وكل منها مرتبط بالمستخدم ولا يُستخدم للتتبع (tracking). جميعها مُصرَّح بها لأغراض App Functionality وAnalytics وDeveloper's Advertising or Marketing، وجميعها عدا معرّف الجهاز لغرض Product Personalization أيضاً. لا يُصرَّح بأي موقع: لا يقرأ SDK موقع الجهاز أبداً، والدولة التي يستنتجها خادم Joryio من عنوان IP ليست بيانات يجمعها SDK. يضمّنه Xcode في تقرير الخصوصية لتطبيقك. أما السمات وخصائص الأحداث التي تختار إرسالها فعليك التصريح بها في بيان تطبيقك.

بدء سريع​

1. هيئ SDK​

في AppDelegate.swift:

import Joryio

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

// Initialize with your SDK key
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_SDK_KEY",
apiHost: "https://api-eu1.joryio.com/api"
)

return true
}
العثور على مفتاح SDK

اعثر على مفتاح SDK في لوحة Joryio تحت Settings ← Apps ← [Your App] ← SDK Keys.

يمكن استدعاء كل دالة عامة بأمان قبل initialize(). تُحفظ الاستدعاءات optOut() وoptIn() وwipeData() وupdateConsent(_:) وsetSdkAuthenticationToken(_:) وsetSdkAuthenticationErrorHandler(_:) وsetInAppMessageCallback(_:capabilities:) وتُطبَّق عند بدء SDK. أما الاستدعاءات التي تجمع البيانات أو ترسلها - track وidentify والسمات وaddAlias والاشتراكات وreset وflush واستدعاءات رمز Push وإيصال التسليم والنقر وإلغاء التسجيل وتقارير In-app - فتُوضع في طابور (حتى 1000، ويُحذف الأقدم أولًا، وهو الحد نفسه في كل SDKs الخاصة بـ Joryio) وتُنفَّذ بالترتيب بعد نجاح initialize(). وحتى ذلك الحين تُعيد دوال القراءة قيمًا فارغة. أما الاستدعاء الذي يجمع البيانات - حدث أو سمة أو alias أو اشتراك أو إيصال Push أو نقرة عليه أو تقرير In-app - وهو يُجرى بينما المستخدم ألغى الاشتراك في التتبع أو الموافقة مرفوضة، فيُحذف فورًا: لن يرسله optIn() أو .granted لاحق أبدًا. ويظل identify محفوظًا، حتى تحمل طلبات السحب المستخدم الصحيح. القرارات المتخذة قبل initialize() - optOut() وoptIn() وupdateConsent(_:) وwipeData() - تسري قبل أي شيء يفعله SDK عند البدء (Session Start ومزامنة In-app وإعادة تسجيل Push). ويتغلب optOut() / optIn() المستدعى قبل initialize() على optOut في الإعداد. وتُجيب isUserOptedOut() وgetTrackingConsent() مما هو مخزَّن حتى قبل initialize(). وحين لم يُتخذ أي قرار بعد، تُجيب getTrackingConsent() بـ .granted - وهي الموافقة التي يبدأ بها تطبيق بلا لافتة موافقة. وإذا رفض initialize() مفتاح SDK أو المضيف، يُجيب registerPushToken(_:onResult:) فورًا بـ success: false بدل الانتظار.

الإقلاع قبل أول فتح للقفل. لا ينتظر إلا الإقلاع الذي يسبق فتح قفل الجهاز مرة واحدة منذ تشغيله (Push في الخلفية أو إيقاظ بسبب الموقع بعد إعادة التشغيل مباشرة): فقبل ذلك لا يمكن قراءة مخزن SDK. يكتمل initialize() من تلقاء نفسه عند ذلك الفتح الأول، وتبقى الاستدعاءات التي تجري في الأثناء في الطابور، وتكون isInitializationPending مساوية لـ true أثناء الانتظار - وهذا ليس فشلًا. أما الجهاز المقفل فقط، الذي فُتح قفله مرة منذ تشغيله، فيبدأ فورًا: يُعالَج Push الصامت على هاتف مقفل بشكل طبيعي.

2. تتبع الأحداث​

// Basic event
Joryio.shared.track("Button Tapped")

// Event with properties
Joryio.shared.track("Product Viewed", properties: [
"product_id": "abc123",
"product_name": "Wireless Headphones",
"price": 99.99,
"category": "Electronics"
])

// Screen view
Joryio.shared.trackScreen("ProductDetail", properties: [
"product_id": "abc123"
])

3. تعريف المستخدمين​

أرسل أرقام الهاتف مع + ورمز الدولة

أرسل أرقام الهاتف دائمًا بالصيغة الدولية الكاملة (E.164): + ثم رمز الدولة ثم الرقم - مثل +972501234567. الرقم الذي لا يبدأ بـ + يُقرأ وفق الدولة الافتراضية لأرقام الهاتف في مساحة العمل؛ وإذا لم تُعيَّن، أو أمكن قراءة الأرقام بطريقتين، يُحفظ غير موثّق ولا يُستخدم للرسائل. كيف تُعالَج أرقام الهاتف

// Identify a user
Joryio.shared.identify("user-123")
Joryio.shared.setAttributes([
"email": "[email protected]",
"name": "John Doe",
"plan": "premium"
])

// Set attributes later
Joryio.shared.setAttribute("last_purchase", value: Date())
Joryio.shared.incrementAttribute("lifetime_value", by: 99.99)

// A second identifier for the same person - works before identify() too
Joryio.shared.addAlias("loyalty_id", name: "L-12345")

// On logout
Joryio.shared.reset()

يضم identify السجل المجهول للجهاز إلى المستخدم على الخادم، ويعيد تسجيل رمز Push باسمه، ويعيد مزامنة رسائل In-app.

تعريف مستخدم مختلف دون reset() بينهما يُعامَل كتسجيل خروج ثم تسجيل دخول فيما يخص ما تركه المستخدم السابق: عمليات كتابة السمات التي لم تُرسل بعد تخرج بمعرّفاته هو، لا بمعرّفات المستخدم الجديد أبدًا، ومع مصادقة SDK يبقى رمزه مرتبطًا بعناصره في الطابور. وما دام الشخص قد ألغى الاشتراك في التتبع أو كانت الموافقة مرفوضة، فإن identify يحتفظ بمعرّف المستخدم على الجهاز فقط، كي يحمل إلغاء الاشتراك أو سجل opt-out أو إلغاء تسجيل Push المستخدم الصحيح - لا يُرسل شيء ولا يُتتبَّع ولا يُكتب في أي ملف شخصي. ويُنفَّذ identify العادي حين يُسمح بالتتبع من جديد؛ وينسى reset() وwipeData() المعرّف المحفوظ.

يبدأ reset هوية مجهولة جديدة فورًا: معرّف مجهول جديد، بلا معرّف مستخدم، وبلا عمليات كتابة سمات معلّقة، وبلا حالة In-app، وجلسة جديدة يُعلن عنها بحدث Session Start. استدعاء identify في السطر التالي مباشرة يستخدم الهوية الجديدة بالفعل. يُعاد تسجيل رمز Push تحتها وتُعاد مزامنة رسائل In-app. أما الأحداث التي دخلت الطابور قبل إعادة التعيين فتُرسل كما هي، بالمعرّفات التي سُجّلت بها.

الاستدعاء الذي تنقصه قيمة إلزامية - identify("") أو track("") أو مفتاح سمة فارغ - يسجّل تحذيرًا ولا يفعل شيئًا. والرقم غير المنتهي (NaN أو اللانهاية) في خصائص الحدث أو قيم السمات يُحذف مع تحذير في السجل، ويمضي باقي الاستدعاء.

تستخدم تسميات addAlias أحرفًا لاتينية صغيرة وأرقامًا وشرطات سفلية. الاسم البديل (alias) الذي يُضاف لزائر مجهول يبقى في ملفه الشخصي عند تعريفه.

أين تُخزَّن المعرّفات​

القيمةمكان التخزينبعد حذف التطبيق
المعرّف المجهول، معرّف الجهاز، الجلسةUserDefaultsتُحذف
opt-out، رفض موافقة محفوظ، قرارات اتُّخذت قبل initialize، سجل عرض رسائل In-appUserDefaultsتُحذف
معرّف المستخدم، رمز جهاز APNs، عمليات كتابة السمات غير المرسلة، عمليات إلغاء تسجيل Push المعلّقة (تحمل الرمز)، معرّف مستخدم محفوظ أثناء opt-out، رمز مصادقة SDK لمستخدم سجّل خروجه (حتى تُرسل عناصره في الطابور أو تُحذف)Keychain (هذا الجهاز فقط، بعد أول فتح للقفل)تُحذف - يكتشف SDK إعادة التثبيت ويمسح عناصره في Keychain
الأحداث في الطابور؛ عمليات كتابة identify وalias والاشتراك وopt-out التي تنتظر الخادم؛ عمليات كتابة السمات التي تركها مستخدم سابق حين عرّف التطبيق شخصًا آخر دون reset()SQLite في Application Support، مستثناة من النسخ الاحتياطيةتُحذف

كل مفتاح يكتبه SDK في UserDefaults يبدأ بـ jry_ أو joryio_.

بيانات الجلسة التلقائية​

يثري iOS SDK أحداث Session Start ببيانات الجهاز والبيئة:

  • $device_id
  • $platform، ios
  • $model
  • $os_name / $os_version
  • $app_version / $build_number
  • $bundle_id
  • $screen_width / $screen_height
  • $locale
  • $language / $languages
  • $timezone
  • country، ISO-3166-1 alpha-2، مشتقة من IP عند بدء الجلسة.

إشعارات Push​

متى تطلب الإذن

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

الـ SDK لا يعرض الطلب نيابةً عنكم أبدًا. وإذا قررتم أنكم تريدونه عند الإقلاع، فعّلوا requestPushPermissionAtLaunch - فهو معطّل افتراضيًا وليس غير متاح.

فعّل إشعارات Push لإرسال رسائل مستهدفة عبر Apple Push Notification Service، APNS.

1. الإعداد في AppDelegate​

import Joryio

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize SDK
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api"
)

// Request push permissions
Task {
let granted = await Joryio.shared.requestPushPermission()
if granted {
print("Push notifications enabled")
}
}

return true
}

// Handle device token registration
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Joryio.shared.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

// Handle registration failure
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
Joryio.shared.didFailToRegisterForRemoteNotifications(error: error)
}

// Handle received push notification
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Joryio.shared.didReceiveRemoteNotification(userInfo, completionHandler: completionHandler)
}
}

يصبح SDK هو delegate لـ UNUserNotificationCenter عند initialize() ويحتفظ بالـ delegate الذي عيّنه تطبيقك قبله. يتعامل فقط مع إشعارات Joryio (مفتاح joryio في الـ payload) - فيعرضها في المقدمة ويعالج النقر عليها - ويمرّر كل إشعار آخر، وكذلك callback الإعدادات، إلى الـ delegate الخاص بك دون تغيير. إذا عيّنت delegate خاصًا بك بعد initialize()، فأبلغ عن نقرات إشعارات Joryio بنفسك عبر trackPushClick(trackingId:).

didRegisterForRemoteNotifications(deviceToken:) هو أيضًا نقطة الدخول لمحرك يستقبل رمز APNs بنفسه - إذ يمرّر جسر Unity إليه الرمز من kUnityDidRegisterForRemoteNotificationsWithDeviceToken. الرمز الذي يصل قبل initialize() يُحفظ ويُسجَّل عند بدء SDK.

لا يُسجَّل الرمز لدى Joryio إلا ما دام المستخدم مشتركًا في التتبع (opted in) وكانت موافقة التتبع ممنوحة. وإلا فإنه يبقى على الجهاز ويُسجَّل عندما يتغير ذلك. وتتبع إيصالات التسليم وتتبع النقرات القاعدة نفسها.

2. ضبط APNS في لوحة التحكم​

لإرسال إشعارات Push، أدخل بيانات اعتماد APNs في تطبيق iOS في Joryio:

  1. انتقل إلى الإعدادات ← التطبيقات وانقر أيقونة إشعارات Push (الجرس) في صف تطبيق iOS.
  2. اختر Environment: التطوير (Sandbox) للإصدارات التي تشغّلها من Xcode، والإنتاج لإصدارات App Store وTestFlight.
  3. اختر مبنية على رمز (.p8 Key) وأدخل Key ID وTeam ID ومحتوى مفتاح المصادقة .p8.
  4. انقر حفظ الإعداد.

للحصول على تعليمات مفصلة لإنشاء مفتاح المصادقة في حساب Apple Developer، راجع الحصول على بيانات اعتماد APNs.

3. خصائص Push​

// Check if push is enabled - true exactly when the status is "granted"
let isEnabled = await Joryio.shared.isPushEnabled()
let status = await Joryio.shared.getPushPermissionStatus() // "granted", "denied" or "notDetermined"

// Get device token
if let token = Joryio.shared.getDeviceToken() {
print("Device token: \(token)")
}

// Badge management
Joryio.shared.updateBadgeCount(5)
Joryio.shared.clearBadge()

// Unregister from push - removes the token on the Joryio server and with iOS.
// Holds across launches until registerPushToken(_:) or requestPushPermission().
Joryio.shared.unregisterPush()

// Your own push stack: hand the token over and report what happened
Joryio.shared.registerPushToken(hexToken) { result in
// result.success, result.message, result.isUnauthorized, result.isRetryable
}
Joryio.shared.reportPushDelivered(trackingId)
Joryio.shared.trackPushClick(trackingId: trackingId)

يُحتسب التفويض المؤقت (provisional) والعابر (ephemeral) على أنه "granted". وتستخدم سمة المستخدم push_permission القيم نفسها، مع not_determined مقابل "notDetermined".

يجب أن يكون الرمز هو رمز جهاز APNs كسلسلة hex. وأي شيء آخر - مثل رمز تسجيل Firebase ‏(FCM) - يُرفض: يُبلغ onResult عن success: false ويسجّل SDK السبب. يبقى unregisterPush() ساريًا عبر مرات التشغيل: الرمز الذي يسلّمه iOS في تشغيل لاحق يُحفظ على الجهاز لكنه لا يُسجَّل، حتى يستدعي التطبيق registerPushToken(_:onResult:) أو requestPushPermission() من جديد. يستدعي registerPushToken(_:onResult:) الدالة onResult مرة واحدة بالنتيجة؛ والاستجابة 200 مع success: false تُبلَّغ كفشل، لا كنجاح أبدًا. الدالتان reportPushDelivered(_:) وtrackPushClick(trackingId:) مخصّصتان لتطبيق يعالج الإشعارات بنفسه، فلا يصل أي منها إلى معالجات SDK.

صور إشعارات Push​

لا يستطيع APNs إرفاق صورة بمفرده؛ بل يجب أن يفعل ذلك Notification Service Extension في تطبيقك. أضف واحدًا (File ← New ← Target ← Notification Service Extension) واربط منتج الامتداد بذلك الهدف فقط:

  • Swift Package Manager: JoryioNotificationServiceExtension
  • CocoaPods: pod 'Joryio/NotificationServiceExtension' داخل كتلة هدف الامتداد

لا تربط Joryio بالامتداد: فهو يستدعي UIApplication.shared الذي لا يجوز لامتدادات التطبيقات استخدامه، ويُدخل SQLite إلى عملية محدودة بـ 24 MB. منتج الامتداد بلا تبعيات. استبدل الصنف المُنشأ بما يلي:

import JoryioNotificationServiceExtension // CocoaPods: import Joryio

class NotificationService: JoryioNotificationService {}

ينزّل الرابط الموجود في المفتاح jry_image_url من الـ payload (أو attachment-url) ويرفقه.

رسائل In-app​

اعرض رسائل In-app مستهدفة للمستخدمين وفق سلوكهم وسماتهم.

رموز التسليم (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 الحملات تلقائيًا من الخادم:

  • عند الإقلاع وعند دخول التطبيق المقدمة - مرة واحدة على الأكثر لكل syncInterval (الافتراضي 300 ثانية).
  • فورًا عند تعريف المستخدم أو إعادة تعيينه، وعند النقر على إشعار Push، وعند وصول Push مزامنة صامت، وعند تغيّر سمة تراقبها حملة.

تزامن syncInAppCampaigns() عند الطلب ولا تخضع لتحديد المعدل.

تمر الرسائل التي يطلقها حدث أو نقر على إشعار Push أو عتبة سمة عبر حدود التكرار نفسها وقاعدة الرسالة الواحدة في كل مرة نفسها مثل أي رسالة أخرى.

التحكم اليدوي بالحملة​

// Manually sync campaigns from server
await Joryio.shared.syncInAppCampaigns()

// Manually trigger campaign evaluation
await Joryio.shared.evaluateInAppCampaigns()

// Reset displayed campaigns (for testing)
Joryio.shared.resetDisplayedCampaigns()
استدعاءات الاختبار مقيّدة بـ enableDebug

evaluateInAppCampaigns() وresetDisplayedCampaigns() تغيّران حالة حقيقية - إحداهما قد تفرض رسالة على الشاشة والأخرى تمسح حدود التكرار الفعلية - لذلك يتم تجاهلهما ما لم يُهيّأ الـ SDK بـ enableDebug: true؛ في إصدار release يُسجَّل تحذير ولا يحدث شيء. أدوات التشخيص للقراءة فقط (getQueueSize() وgetSessionInfo() وgetIdentity() وgetDeviceToken()) غير مقيّدة. أما syncInAppCampaigns() فهي API عادية وتعمل دائمًا.

نوعان من المحتوى​

تصل كل حملة بأحد شكلين من المحتوى، وتعرض حزمة SDK كلًا منهما بطريقة مختلفة:

المحتوىما هوكيف يُعرض
الأصلي (Native)بيانات منظمة - عنوان ونص وصورة وأزرارعناصر UIKit حقيقية، بلون التمييز و‑Dynamic Type والوضع الداكن و‑VoiceOver في تطبيقك. بدون web view.
HTMLHTML و‑CSS كتبها المؤلفWKWebView داخل الرسالة، تحت Content-Security-Policy صارمة.

السماح برسائل HTML​

رسائل HTML معطلة افتراضيًا. تعرض رسالة HTML شيفرة كتبها المؤلف في web view داخل تطبيقك، لذا فإن تفعيلها قرار يتخذه فريق التطبيق - وليس شيئًا يُفعَّل من منصة تسويق:

let config = JoryioConfig(
inApp: InAppConfig(allowHtmlJsInAppMessages: true) // الافتراضي: false
)

Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api",
config: config
)

تركه معطلًا لا يعطل الرسائل داخل التطبيق. تستمر الرسائل الأصلية في الظهور، لأنها بيانات ترسمها عناصر الحزمة نفسها - دون أي مفسّر. تُتجاهل حملات HTML وتُسجَّل.

هذا الإعداد يحكم مُصيِّر الحزمة نفسها. إذا كان تطبيقك يرسم الرسائل داخل التطبيق بنفسه عبر setInAppMessageCallback(_:capabilities:)، فإن web view الخاص بالحزمة لا يعمل إطلاقًا، ولذلك لا ينطبق الإعداد على هذا المسار - صرِّح بـ content.html ضمن capabilities وستُسلَّم حملات HTML إلى الـ callback الخاص بك سواء كان الإعداد مفعّلًا أم لا. المُصيِّر لك، والقرار لك.

يُعرض مستند الرسالة تحت Content-Security-Policy تحظر كل السكربتات والإطارات والوصول إلى الشبكة وإرسال النماذج - وهي القواعد نفسها في web SDK. لا يعمل سوى joryioBridge الخاص بـ SDK، لذا تعمل الروابط والأزرار وسمات data-joryio-action واستدعاءات الجسر ونماذج data-jry-form، لكن <script> الخاص بالمؤلف ومعالجات on* المضمّنة وتضمينات <iframe> لا تُنفَّذ على iOS (ولا على الويب).

إذا كانت سياسة الأمان لديك تمنع عرض HTML مكتوب خارجيًا داخل العملية، فاترك الإعداد معطلًا وأنشئ حملاتك كرسائل أصلية.

عرض الرسائل بنفسك​

تتولى الحزمة رسم الرسائل الأصلية نيابةً عنك، لكن يمكنك تولّي الأمر بالكامل - وهو المنفذ نفسه الذي يسميه غيرنا custom view factory. نفِّذ InAppMessagePresenter ثم عيّنه:

Joryio.shared.inAppPresenter = MyPresenter()

لا يعمل الاكتشاف التلقائي إلا عندما تكون inAppPresenter بقيمة nil، لذا يحل presenter الخاص بك محل المُصيِّر المدمج بدل أن يتعارض معه. عيّنه قبل initialize أو بعده.

يستلم presenter الحملة وcontent محلولة مسبقًا - يُعرض Liquid على الخادم، وتصل الحقول الأصلية كنص عادي. أعِد true من present(campaign:in:delegate:) فقط عندما تكون الرسالة على الشاشة: عندها يبلّغ SDK عن "displayed" بنفسه ويحتسب الظهور ضمن حد تكرار الحملة. بلّغ عن النقرات والإغلاق عبر delegate الذي تستلمه - messageViewClicked(_:action:url:) مع معرّف الزر أو الرابط، وmessageViewDidDismiss(_:) عند إغلاق الرسالة. لا تستدعِ trackInAppImpression من presenter: سيُبلَّغ عن الرسالة مرتين.

لعرض الرسائل بواجهتك الخاصة دون presenter، استخدم callback بدلًا من ذلك:

Joryio.shared.setInAppMessageCallback({ campaign in
showMyMessage(campaign)
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "displayed")
}, capabilities: ["content.native"])

// When a button is tapped, and when the message closes:
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "clicked", elementId: "buy_now")
Joryio.shared.trackInAppImpression(campaignId: campaign.id, action: "dismissed")

// Hand display back to the SDK's own renderer:
Joryio.shared.clearInAppMessageCallback()

قيمة action هي "displayed" أو "clicked" أو "dismissed" أو "converted"؛ وتُرفض أي قيمة أخرى مع رسالة في السجل. يسمّي elementId اختياريًا الزر أو الرابط في حالة النقر. بلاغ "displayed" الخاص بك هو الوحيد الذي يُرسل لتلك الرسالة، وهو ما يحتسب الرسالة ضمن حد تكرار الحملة - فالرسالة التي سُلّمت إلى callback ولم تُعرض لا تستهلك منه شيئًا. يتيح "dismissed" لـ SDK تسليم الرسالة التالية: فالرسالة التي أبلغت عنها بـ "displayed" تحتفظ بالعرض حتى تُبلغ بـ "dismissed"، فلا يُرسم فوقها شيء. ودون أي بلاغ يحرر SDK نفسه بعد 30 ثانية؛ أما الرسالة التي أُبلغ عنها "displayed" ولم يُبلغ عنها "dismissed" أبدًا فتشغل العرض 5 دقائق على الأكثر. أبلغ عن "dismissed" كلما غادرت الرسالة الشاشة، حتى بعد النقر. والرسالة التي تنتظر مهلة عرضها لا تُسلَّم إذا عُرّف المستخدم بوصفه شخصًا آخر أو ألغى الاشتراك في التتبع أو تغيّرت الموافقة.

منذ الإصدار 1.3.2. يُثبَّت callback المعيَّن قبل initialize() قبل المزامنة الأولى. أما إذا عيّنته بعد initialize()، فقد تنتهي المزامنة الأولى قبله فترسم الحزمة تلك الرسالة بنفسها؛ مرّر InAppConfig(holdInAppMessagesForHandler: true) لمنع ذلك. عندها تُحتجز الرسالة الجاهزة قبل تعيين callback - لا تُرسم، ولا يُبلغ عنها، ولا تُحتسب ضمن أي حد للتكرار - وتُسلَّم إلى callback لحظة تعيينه. أما استدعاء clearInAppMessageCallback() بدلًا من ذلك فينهي الاحتجاز وترسم الحزمة الرسالة المحتجزة.

اسم الصنف يختلف بحسب مدير الحزم

لا تضبط هذا بنفسك، لكن معرفته مفيدة حين لا يظهر شيء. تعثر الحزمة على مُصيِّرها المدمج بالاسم وقت التشغيل، والوحدة التي يوجد فيها ذلك الاسم تعتمد على طريقة الدمج:

طريقة التحزيمالصنف الذي تبحث عنه الحزمة
SwiftPM‏JoryioUI هدف مستقلJoryioUI.DefaultInAppMessagePresenter
CocoaPods‏Joryio/UI هو subspec، والـ subspecs تتشارك وحدة الـ podJoryio.DefaultInAppMessagePresenter

يُجرَّب الاسمان معًا. وإذا لم تظهر أي رسالة داخل التطبيق فابحث عن تحذير الحزمة "No in-app presenter found"، فهو يعني أن منتج الواجهة غير مرتبط (pod 'Joryio/UI' أو منتج JoryioUI في SwiftPM)، لا أن الحملة لم تصل. والحالتان تبدوان متطابقتين من الخارج.

أما عارضك الخاص فلا يحتاج شيئًا من ذلك: عيّنه ولن يعمل الاكتشاف أصلًا.

أنواع الرسائل​

تدعم الحزمة 5 أنواع من الرسائل:

  1. Modal - وسط الشاشة مع خلفية معتمة
  2. Banner - أعلى الشاشة
  3. Slide-Up - إشعار صغير من الأسفل
  4. Full-Screen - رسالة تملأ الشاشة
  5. Custom - التطبيق يحدد الموضع

تستقبل رسائل Banner وSlide-Up اللمسات داخل الرسالة فقط، فيبقى باقي تطبيقك قابلًا للاستخدام خلفها. أما رسائل Modal وFull-Screen فتحجب الشاشة.

حدود التكرار​

تحترم الرسائل قواعد التواصل على مستوى مساحة العمل وحدود التكرار على مستوى الحملة:

  • الحد الأقصى لمرات الظهور لكل نافذة زمنية.
  • أقل تأخير بين الرسائل.
  • حدود تكرار لكل حملة.

متى تُحتسب الرسالة معروضة​

تُحتسب الرسالة معروضة - أي يُرسَل in_app.displayed ويُستهلك حد التكرار للحملة - فقط عندما يكون الشخص قد تمكّن فعلًا من رؤيتها. المدة التي يجب أن تبقى فيها على الشاشة هي IN_APP_DISPLAY_DWELL_MS (1000 مللي ثانية)، وهو الثابت نفسه في كل SDK من Joryio.

  • لا يعرض الـ SDK رسالة إلا عندما يكون التطبيق نشطًا ويكون الـ view controller العلوي مستقرًا - ليس في منتصف العرض أو الإغلاق أو النقل. وإلا فإنه ينتظر ويحاول مجددًا.
  • تُحتسب الرسالة معروضة بعد بقائها على الشاشة ثانية واحدة والتطبيق ما زال نشطًا. إذا انتقل التطبيق إلى الخلفية قبل ذلك، تبدأ الثانية من جديد عند عودته.
  • إذا غادرت الرسالة الشاشة قبل ذلك، لا يُرسَل شيء ولا يُستهلك الحد، فتبقى مؤهلة.
  • النقر أو الإغلاق قبل ذلك يُحتسب مشاهدة: يُرسَل displayed أولًا ثم النقر أو الإغلاق. إعادة الضبط أو تغيير المستخدم أو إلغاء الموافقة قبل ذلك لا يُرسل شيئًا.
  • إذا كنت تعرض الرسائل بنفسك: أرسل displayed وفق القاعدة نفسها - بعد أن تبقى الرسالة ظاهرة لمدة InAppDisplay.IN_APP_DISPLAY_DWELL_MS، أو مباشرة قبل نقر أو إغلاق يأتي أبكر.

خيارات الإعداد​

خصص سلوك SDK باستخدام JoryioConfig:

JoryioConfig(
// User Identification
userId: String?, // Initialize with known user ID
anonymousId: String?, // Custom anonymous ID

// Batching & Performance
batchSize: Int, // Default: 50 (at most 500 per request)
flushInterval: TimeInterval, // Default: 5.0 seconds (minimum 1.0)
sendImmediately: Bool, // Default: false
maxQueueSize: Int, // Default: 1000

// Session Management
sessionTimeout: TimeInterval, // Default: 1800 (30 minutes)
trackSessionStart: Bool, // Default: true

// Push
requestPushPermissionAtLaunch: Bool, // Default: false - prefer a primer

// In-App Messaging
inApp: InAppConfig, // Default: InAppConfig() - see below

// Network & Retry
maxRetries: Int, // Default: 3
retryBackoffMs: Double, // Default: 1000.0
requestTimeout: TimeInterval, // Default: 10.0

// SDK Authentication (opt-in)
enableSdkAuthentication: Bool, // Default: false
sdkAuthenticationToken: String?, // Default: nil

// Privacy & GDPR
optOut: Bool, // Default: false (optOut() persists regardless)
trackingConsent: TrackingConsent?, // Default: nil (unset) - see Tracking consent

// Debugging
enableDebug: Bool, // Default: false
logLevel: LogLevel? // Default: nil - .debug with enableDebug, otherwise .error
)
InAppConfig(
enabled: Bool, // Default: true - false turns in-app off
allowHtmlJsInAppMessages: Bool, // Default: false
syncInterval: TimeInterval, // Default: 300 - seconds between automatic syncs
onMessageDisplay: ((InAppMessage) -> Void)?, // after the SDK displays a message
onMessageClick: ((InAppMessage, String) -> Void)?, // with the tapped element's name
onMessageDismiss: ((InAppMessage) -> Void)?, // after a message is dismissed
holdInAppMessagesForHandler: Bool // Default: false - hold messages until setInAppMessageCallback (since 1.3.2)
)

تعمل الـ callbacks الثلاثة على الخيط الرئيسي (main thread).

الخصائص المتقدمة​

إدارة سمات المستخدم​

// Set multiple attributes
Joryio.shared.setAttributes([
"age": 28,
"city": "San Francisco",
"premium": true
])

// Set single attribute
Joryio.shared.setAttribute("language", value: "en")

// Increment numeric attribute
Joryio.shared.incrementAttribute("page_views", by: 1)
Joryio.shared.incrementAttribute("total_spent", by: 29.99)

// Remove attribute
Joryio.shared.unsetAttribute("temporary_flag")

عناصر تحكم الخصوصية​

// Stop collecting. PERSISTED - survives an app restart.
Joryio.shared.optOut()

// Opt back in
Joryio.shared.optIn()

// Check opt-out status
if Joryio.shared.isUserOptedOut() {
print("User has opted out")
}

// Delete everything the SDK stored on this device.
// SEPARATE from optOut(): "stop collecting" and "delete what you have" are
// different requests. This is the one an erasure request needs. It does NOT
// opt the user out - call optOut() as well if that is also intended.
Joryio.shared.wipeData()

// Get identity info
let (userId, anonymousId) = Joryio.shared.getIdentity()
print("User: \(userId ?? "anonymous"), Anonymous ID: \(anonymousId)")

ما الذي يفعله optOut() بالضبط:

يوقف الجمعنعم - كل مسارات الكتابة: track وidentify والسمات (بما فيها الزيادات وعمليات unset) والأسماء المستعارة والاشتراك وتسجيل Push وإيصالات التسليم ومرات ظهور In-app. أما سحب الموافقة فيُرسل دائمًا: إلغاء الاشتراك وسجل إلغاء التتبع وإلغاء تسجيل Push
يبقى بعد إعادة التشغيلنعم - يُخزَّن العلم على الجهاز ويُقرأ قبل جمع أي شيء. ولا يرتبط بمفتاح SDK، فيبقى أيضًا عند تغيير المفتاح. وتُخزَّن optOut: true في الإعداد بالطريقة نفسها
يحذف ما في الطابورنعم - تُحذف الأحداث في الطابور وعمليات كتابة السمات غير المؤكدة وعمليات كتابة identify وalias والاشتراك المنتظرة، ولا تُسلَّم لاحقًا. ويُحتفظ بعمليات سحب الموافقة المنتظرة وتُسلَّم
يُبلغ الخادمنعم - سمة ملف التعريف tracking_opted_out (true؛ ويرسل optIn() القيمة false). تُرسل فقط حين يتغير إلغاء الاشتراك - بما في ذلك optOut: true في الإعداد أو استدعاء قبل initialize() - ويُعاد إرسالها حتى يحفظها الخادم. تُرسل أيًا كانت الموافقة - حتى وهي معلّقة أو مرفوضة
يحذف البيانات المخزنةلا - استخدم wipeData()

السمة tracking_opted_out هي سجل، لا إلزام: تُحفظ في ملف التعريف كي تتمكن الحملات من الاستثناء بناءً عليها. أما الحجب من جهة الخادم فهو إعداد منفصل.

ما الذي يمحوه wipeData(): معرّف المستخدم والمعرّف المجهول، ومعرّف الجهاز، والأحداث في الطابور، وعمليات كتابة السمات غير المرسلة، وحالة In-app، والجلسة، ورمز Push - الذي يُلغى تسجيله أيضًا على خادم Joryio، فلا يُرسل شيء آخر إلى هذا التثبيت. ويُحتفظ بإلغاء الاشتراك في التتبع: محو بيانات شخص لا يعيده إلى التتبع. ويبدأ الحدث التالي (أو عودة التطبيق التالية إلى الواجهة) جلسة جديدة يُعلَن عنها بحدث Session Start. بعد ذلك تبقى الإشعارات متوقفة حتى يستدعي التطبيق registerPushToken مجددًا أو يطلب الإذن من جديد - ولا يُسجَّل تلقائيًا رمز يصل في تشغيل لاحق. ويجري التسجيل وإلغاء التسجيل واحدًا في كل مرة: إعادة تسجيل الرمز نفسه تُلغي إلغاء تسجيله إن لم يصل إلى الخادم بعد، وإلغاء التسجيل غير المؤكد لرمز آخر يُرسل قبل أي تسجيل جديد.

مرّر الموافقة التي يحملها مدير الموافقات لديك عند الإقلاع، وحدّثها عندما يقرر المستخدم:

Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "https://api-eu1.joryio.com/api",
config: JoryioConfig(trackingConsent: .pending)
)

// Later, from your consent banner:
Joryio.shared.updateConsent(.granted) // or .denied
الموافقةالسلوك
.grantedعادي
.pendingتُحفظ الأحداث وعمليات كتابة السمات على الجهاز (حتى maxQueueSize) ولا يُرسل شيء - لا Session Start، ولا مزامنة In-app، ولا تسجيل Push. تُرسل .granted ما حُفظ؛ وتتجاهله .denied
.deniedلا يُجمع شيء، ويُمسح كل ما حُفظ

مفتاحان مستقلان يقرران ما إذا كان يُجمع أي شيء: الموافقة التي يبلغ عنها شريط الموافقة لديك، وإلغاء الاشتراك (opt-out) الخاص بالشخص نفسه. لا يحدث الجمع إلا حين تكون الموافقة .granted ولم يُلغِ الشخص اشتراكه - أي "لا" تتغلب.

  • .denied تُخزَّن على الجهاز وتبقى سارية عبر مرات الإقلاع. لا يرفعها إلا updateConsent(.granted) صريح، أو trackingConsent: .granted يُمرَّر في الإعداد.
  • القيمة الافتراضية لـ trackingConsent غير محددة (nil). غير محددة مع عدم وجود شيء مخزَّن تتصرف مثل .granted، فيعمل التطبيق الذي لا يملك شريط موافقة؛ وغير محددة مع .denied مخزَّنة تبقى مرفوضة.
  • .pending لا تُخزَّن: تسري على هذا الإقلاع فقط ولا تتغلب أبدًا على .denied، سواء كانت مخزَّنة أو عُيّنت قبل ذلك قبل initialize().
  • الموافقة لا ترفع إلغاء الاشتراك أبدًا. وحده optIn() يفعل ذلك، وoptOut() لا يغيّر الموافقة المخزَّنة أبدًا.
  • تُعيد getTrackingConsent() الموافقة السارية؛ وتعكس isUserOptedOut() إلغاء الاشتراك فقط.
  • الجلسة التي بدأت حين لم يكن الجمع مسموحًا يُعلَن عنها بحدث Session Start حالما يُسمح به - لاحقًا في التشغيل نفسه، أو في تشغيل لاحق كجلسة جديدة.
ملاحظة

تم التحديث بالإنجليزية - الترجمة قيد الإعداد.

تفريغ الطابور يدويًا​

// Flush immediately (e.g., before app termination)
Joryio.shared.flush()

// Check queue size
let queueSize = Joryio.shared.getQueueSize()
print("Pending events: \(queueSize)")

تُرسل الأحداث في دفعات من 500 حدث على الأكثر ونحو 900 KB. إذا رفض الخادم دفعة باعتبارها غير صالحة (HTTP 400 أو 413 أو 422)، يقسمها SDK ويعيد إرسال النصفين، فلا يستطيع حدث سيئ واحد تعطيل البقية؛ والحدث الذي يُرفض وحده يُحذف ويُسجَّل. الأحداث التي يبلغ الخادم عن فشلها تُحفظ ويُعاد إرسالها. في حالات HTTP 429 و5xx وأعطال الشبكة يُحتفظ بكل شيء ويُعاد المحاولة بتراجع أسي مع احترام Retry-After. أما HTTP 402 أو 403 فيوقف الإرسال لبقية الجلسة ويحتفظ بالأحداث. الإخفاقات لا تحذف الأحداث أبدًا؛ وحده maxQueueSize يحذفها، الأقدم أولًا. يحذف SDK عند track()، مع سطر في السجل، الحدث الذي يعرف أن الخادم سيرفضه - اسم فارغ أو أطول من 255 حرفًا، أو خصائص تتجاوز 50 KB مسلسلة (تُقاس كما يقيسها الخادم)، أو أكثر من 200 مفتاح في المستوى الأعلى أو تداخل أعمق من 5 مستويات؛ وتُحذف أيضًا الأحداث التي يذكرها الخادم في rejectedIndices ولا يُعاد إرسالها. يتخطى flush() التراجع الخاص بـ SDK لكنه لا يتخطى أبدًا Retry-After الذي يطلبه الخادم. مع مصادقة SDK لا تخلط الدفعة مستخدمَين أبدًا، وتُحذف الأحداث المنتظرة لمستخدم سجّل خروجه حين يرفضها الخادم لأي سبب من أسباب مصادقة SDK، فلا تعطّل أحداث المستخدم التالي.

عمليات الكتابة التي يجب أن تصل إلى الخادم​

تُحفظ identify(_:) وaddAlias(_:name:) واستدعاءات الاشتراك وسجل tracking_opted_out والجانب الخاص بالخادم من unregisterPush() / wipeData() على الجهاز حتى يؤكدها الخادم - عبر إعادة التشغيل وعبر reset() (كل منها يحمل المعرّفات التي أُنشئ تحتها). تُرسل بالترتيب، واحدة في كل مرة، والتغيير الأحدث من الشخص نفسه للقناة نفسها أو المجموعة نفسها يحل محل التغيير الذي ما زال ينتظر، فلا تصل محاولة قديمة بعده أبدًا. والكتابة التي أُنشئت والمستخدم مجهول وأُرسلت بعد identify(_:) تخرج بمعرّف المستخدم، ما دام المعرّف المجهول الذي أُنشئت تحته هو معرّف هذا الجهاز. الكتابة التي يرفضها الخادم نهائيًا (HTTP 400 أو 403 أو 409 أو 413 أو 422) تُحذف وتُسجَّل؛ وتغيير الاشتراك الذي يُجاب بـ 404 (لا يوجد ملف تعريف بعد) يُعاد إرساله بعد إرسال الأحداث المنتظرة، 5 مرات على الأكثر؛ أما success: false ورد 401 لمصادقة SDK للمستخدم الحالي فتُعاد بتراجع، 10 مرات على الأكثر لكل كتابة. انقطاع الشبكة ومهلات الانتظار وHTTP 402 و429 و5xx لا تُحتسب أبدًا على الكتابة. وما دام استقبال البيانات متوقفًا مؤقتًا في مساحة العمل (HTTP 402)، تخرج عمليات سحب الموافقة قبل عمليات الكتابة التي عليها الانتظار. وحين تنتظر 1,000 عملية كتابة، تُفسح الأقدم بينها التي ليست سحبًا للموافقة المجال. يحذف optOut() والموافقة .denied وwipeData() عمليات كتابة identify وalias والاشتراك التي ما زالت تنتظر؛ أما سحب الموافقة - إلغاء الاشتراك وسجل tracking_opted_out وإلغاء تسجيل Push - فيُحتفظ به ويُسلَّم.

مصادقة SDK​

يثبت الرمز هوية المستخدم المعرَّف: يجب أن تحمل الطلبات التي تذكر معرّف مستخدم رمزًا صالحًا. أما الزوار المجهولون فليس لديهم رمز ليقدّموه، لذا تُقبل طلباتهم من دونه، إلا إذا فعّلت مساحة العمل أيضًا الهوية الصارمة (strict identity).

عند تفعيل مصادقة SDK لمساحة عملك، يجب أن يحمل كل طلب استيعاب JWT صادرًا عن خادمك. توفير رمز - في sdkAuthenticationToken أو عبر setSdkAuthenticationToken(_:)، قبل initialize() أو بعده - يفعّل الترويسة، مع enableSdkAuthentication أو بدونه:

Joryio.shared.setSdkAuthenticationToken(tokenFromYourBackend)
Joryio.shared.setSdkAuthenticationErrorHandler { error in
fetchFreshToken { Joryio.shared.setSdkAuthenticationToken($0) }
}

يمسح reset() الرمز. أما الأحداث وعمليات الكتابة التي وضعها المستخدم المغادر في قائمة الانتظار فتُرسل مع رمزه هو، فلا يضيع شيء عند تسجيل الخروج العادي - إذ يُحفظ الرمز في Keychain مرتبطًا بتلك العناصر حتى تُرسل أو تُحذف، فلا يضيع شيء حتى لو أُغلق التطبيق مباشرة بعد تسجيل الخروج. وتعريف مستخدم مختلف دون reset() يفعل الشيء نفسه مع رمز المستخدم السابق، وكذلك تعيين رمز المستخدم التالي عبر setSdkAuthenticationToken(_:): سواء عُيّن قبل identify أو بعده، تحتفظ العناصر التي تركها المستخدم السابق برمزه هو. ويحمل كل طلب الرمز الذي يكون sub فيه هو مستخدم ذلك الطلب، ولا يحمل أبدًا رمزًا يسمّي شخصًا آخر؛ والرمز المعيَّن للمستخدم التالي قبل reset() - حتى حين لا يكون أحد مسجَّل الدخول - يُحفظ له ويُستخدم مجددًا عند identify الخاص به، ويمسحه wipeData() - إلا إذا كان رمز المستخدم الذي تُمحى بياناته (ومنه المستخدم الذي عُرِّف والتتبع متوقف)، أو الرمز المقدَّم لهذا التشغيل حين يُنفَّذ wipeData() استُدعي قبل initialize(). والأحداث وعمليات الكتابة التي تركها مستخدم لم يعد المستخدم الحالي تُحذف حين ترفضها مصادقة SDK، أيًا كان السبب (missing أو sub_mismatch أو expired أو invalid)، فلا تعطّل ما يخص المستخدم التالي. ولا يُبلَّغ معالج الأخطاء إلا بطلبات المستخدم الحالي، وليس أبدًا بما تركه مستخدم سابق. وحتى حين يلغي الشخص الاشتراك في التتبع أو تُرفض الموافقة، عيّن رمز المستخدم عند تسجيل الدخول: فالمستخدم الذي عُرِّف في الأثناء يُعدّ المستخدم الحالي لإلغاء اشتراكه، فإذا رُفض يُطلب من المعالج رمزه ويُحتفظ بإلغاء الاشتراك.

مرجع API​

كل عضو عام في Joryio.shared، باسمه الدقيق في Swift.

الدالةماذا تفعل
initialize(sdkKey:apiHost:config:)تبدأ SDK بمفتاح SDK ومضيف API وJoryioConfig اختياري. ويُتجاهل الاستدعاء الثاني
track(_:properties:)تسجّل حدثًا مع خصائص اختيارية
trackScreen(_:properties:)تسجّل حدث Screen Viewed مع screen_name
identify(_:)تربط الجهاز بمعرّف مستخدم؛ وتعيد تسجيل Push ومزامنة رسائل In-app
addAlias(_:name:)تضيف معرّفًا ثانيًا بوسم، للزوار المجهولين أيضًا
reset()تسجيل الخروج: هوية مجهولة وجلسة جديدتان
setAttributes(_:)تعيّن عدة سمات؛ وقيم nil / NSNull تحذف
setAttribute(_:value:)تعيّن سمة واحدة؛ وnil يحذفها
incrementAttribute(_:by:)زيادة رقمية ذرية (الافتراضي 1)
unsetAttribute(_:)تحذف سمة
getUserId()معرّف المستخدم الحالي، أو nil
getAnonymousId()المعرّف المجهول الحالي
getIdentity()(userId, anonymousId)
getSessionId()معرّف الجلسة الحالية
getSessionInfo()(sessionId, sessionStart, duration)
optOut()توقف كل جمع وإرسال؛ وتُحفظ
optIn()الاستئناف بعد optOut()
isUserOptedOut()هل ألغى المستخدم الاشتراك في التتبع؛ وقبل initialize() مما هو مخزَّن
getTrackingConsent()موافقة التتبع السارية: .granted أو .denied أو .pending
updateConsent(_:)تعيّن موافقة التتبع: .granted أو .pending أو .denied
wipeData()تمحو كل ما خزّنه SDK على الجهاز؛ وتُبقي إلغاء الاشتراك
setSubscription(channel:status:)حالة الاشتراك التسويقي لقناة واحدة
addToSubscriptionGroup(_:channel:)الانضمام إلى مجموعة اشتراك في قناة
removeFromSubscriptionGroup(_:channel:)مغادرة مجموعة اشتراك في قناة
setSdkAuthenticationToken(_:)تقدّم أو تستبدل JWT مصادقة SDK
setSdkAuthenticationErrorHandler(_:)تُستدعى عندما يرفض الخادم JWT؛ ردّ برمز جديد
clearSdkAuthenticationErrorHandler()تزيل هذا المعالج، مثلًا عند إغلاق جسر أو شاشة
flush()ترسل أحداث الطابور الآن
getQueueSize()الأحداث المنتظرة على الجهاز
lastTransportErrorآخر فشل في طلب (JoryioTransportError)، أو nil بعد النجاح
currentApiEndpointعنوان API الأساسي الذي يرسل إليه SDK
isInitializationPendingtrue ما دام initialize() ينتظر أول فتح للقفل منذ تشغيل الجهاز؛ وليس فشلًا
ecommerce(config:)مساعد التجارة الإلكترونية؛ مرّر إعدادًا لتعيين العملة
inAppPresenterInAppMessagePresenter خاص بك، بدل المُصيِّر المدمج
setInAppMessageCallback(_:capabilities:)استلام رسائل In-app ورسمها بنفسك
clearInAppMessageCallback()إعادة الرسم إلى SDK
trackInAppImpression(campaignId:action:elementId:)الإبلاغ عمّا فعلته واجهتك برسالة
submitInAppForm(campaignId:values:)إرسال نموذج معروض في رسالة (async)؛ لا يرمي خطأ أبدًا
syncInAppCampaigns()جلب حملات In-app الآن (async)
evaluateInAppCampaigns()للاختبار فقط، تتطلب enableDebug: تقييم الحملات الآن
resetDisplayedCampaigns()للاختبار فقط، تتطلب enableDebug: نسيان الرسائل التي عُرضت
requestPushPermission()طلب إذن الإشعارات (async)؛ يلغي unregisterPush()
getPushPermissionStatus()"granted" أو "denied" أو "notDetermined" (async)
isPushEnabled()true تمامًا عندما تكون الحالة "granted" (async)
didRegisterForRemoteNotifications(deviceToken:)تمرير رمز APNs من AppDelegate
didFailToRegisterForRemoteNotifications(error:)تمرير فشل تسجيل APNs
didReceiveRemoteNotification(_:completionHandler:)تمرير إشعار بعيد
registerPushToken(_:onResult:)تسجيل رمز hex بنفسك، مع callback للنتيجة؛ يلغي unregisterPush()
getDeviceToken()رمز APNs المخزّن، أو nil
unregisterPush()إزالة الرمز على الخادم ومن iOS؛ ويبقى ساريًا عبر مرات التشغيل
reportPushDelivered(_:)الإبلاغ عن وصول إشعار Push، بواسطة trackingId الخاص به
trackPushClick(trackingId:)الإبلاغ عن نقرة على إشعار Push، بواسطة trackingId الخاص به
updateBadgeCount(_:)تعيين شارة التطبيق
clearBadge()مسح شارة التطبيق
Joryio.pushPermissionAttributeاسم السمة push_permission

أفضل الممارسات​

1. تتبع أحداثًا ذات معنى​

ركز على الأحداث التي تهم نشاطك:

// Good: Specific, actionable events
Joryio.shared.track("Trial Started", properties: ["plan": "premium"])
Joryio.shared.track("Feature Used", properties: ["feature": "export"])

// Avoid: Overly generic events
Joryio.shared.track("Button Tapped") // Too generic

2. تعامل مع دورة حياة المستخدم​

// On login
func handleLogin(userId: String, userInfo: UserInfo) {
Joryio.shared.identify(userId)
Joryio.shared.setAttributes([
"email": userInfo.email,
"name": userInfo.name
])
}

// On logout
func handleLogout() {
Joryio.shared.reset()
}

// On signup - identify() joins the anonymous history to the new user
func handleSignup(userId: String, userInfo: UserInfo) {
Joryio.shared.identify(userId)
Joryio.shared.setAttributes(userInfo.attributes)
}

3. فرّغ في الأحداث الحرجة​

override func applicationWillTerminate(_ application: UIApplication) {
Joryio.shared.flush()
}

استكشاف الأخطاء وإصلاحها​

الأحداث لا تظهر​

  1. تأكد من صحة مفتاح SDK: jry_sdk_ios_*.
  2. فعّل سجل التصحيح: enableDebug: true (يسجّل كل شيء ما لم تضبط logLevel أيضًا).
  3. افحص سجلات console بحثًا عن الأخطاء.
  4. تحقق من اتصال الشبكة.
  5. استدعِ flush() للإرسال فورًا.

Push لا يعمل​

  1. تحقق من حفظ بيانات اعتماد APNs في التطبيق في لوحة التحكم.
  2. تحقق من تسجيل رمز الجهاز.
  3. تأكد من entitlements الصحيحة في Xcode.
  4. اختبر في البيئة الصحيحة، dev مقابل production.
  5. ابحث عن خطأ إرسال Push تجريبي في الحصول على بيانات اعتماد APNs.

أخطاء البناء​

  1. تأكد من هدف نشر iOS 14.0+.
  2. نظف مجلد البناء: Cmd+Shift+K.
  3. حدّث تبعيات Swift Package.

تتبع E-Commerce​

يتضمن iOS SDK متتبعًا مدمجًا للتجارة الإلكترونية لأحداث المنتجات والسلة وإتمام الشراء والطلبات. راجع دليل تتبع E-Commerce العابر لـ SDKs لمعرفة API الكاملة مع أمثلة Swift.

let ecommerce = Joryio.shared.ecommerce(config: EcommerceConfig(currency: "USD"))
ecommerce.purchase(order: EcommerceOrder(
orderId: "ORD-1001",
items: [EcommerceCartItem(productId: "SKU123", name: "Blue Shirt", price: 29.99, quantity: 2)],
value: 59.98
))

الاستدعاء الذي ينقصه ما يمنح الحدث معناه - productId أو orderId أو سعر أو قيمة محدودة أو قائمة items في عملية الشراء (يجوز أن تكون فارغة) - يكتب تحذيرًا في السجل ولا يرسل شيئًا، ولا يرسل أبدًا طلبًا فارغًا أو بقيمة صفر.

ترسل purchase الحدث Order Completed بالمفاتيح التي تقرؤها تقارير الإيرادات: orderId وtotal (إضافة إلى currency والعناصر). إن تتبعت عملية شراء بنفسك عبر track() فاستخدم المفاتيح نفسها - فالمفتاحان order_id وvalue لا يُقرآن، ويُحتسب الطلب بإيراد صفري.

الخطوات التالية​

تسليم السمات​

الدوال setAttributes وsetAttribute وincrementAttribute وunsetAttribute متينة. تبقى عملية الكتابة في الطابور حتى يؤكدها الخادم، لذا فإن السمة التي تُعيَّن والجهاز غير متصل تُسلَّم عند عودة الاتصال بدل أن تضيع. تُرسل الزيادة كفرق ذري، وليس أبدًا كمجموع محسوب على الجهاز.

  • إعادة المحاولة عند كتابة السمة التالية، وعند عودة التطبيق إلى الواجهة، وعند استئناف الإرسال (optIn()، منح الموافقة).
  • التجميع - تتحول سلسلة استدعاءات متتالية إلى طلب واحد (نافذة 800ms). وتخرج الكتابة المنفردة فورًا. الكتابة التي تحدث أثناء وجود طلب قيد الإرسال تنتظره وتليه؛ ولا تلغيه أبدًا، فلا تُحتسب الزيادة مرتين.
  • لا يؤكدها إلا الخادم - الاستجابة 200 مع success: false تُبقي عمليات الكتابة للمحاولة التالية. والكتابة التي يرفضها الخادم كغير صالحة (HTTP 400 أو 413 أو 422) تُقسَم ويُعاد إرسال نصفيها، فلا يعطّل مفتاح خاطئ واحد البقية؛ والكتابة المنفردة التي تُرفض مجددًا تُحذف وتُسجَّل. يُرسل حتى 200 عملية unset في الطلب الواحد، ويتبعها الباقي.
  • nil يحذف - setAttribute("nickname", value: nil)، أو NSNull من جسر، يحذف السمة مثل unsetAttribute.
  • تُحفظ في iOS Keychain، لذا تنجو الكتابة من إنهاء العملية. وتُمسح لحظة تأكيد الخادم، وتُنظَّف عبر optOut() وreset() وwipeData(). الكتابة التي يرفضها الخادم بـ HTTP 403 تُحذف وتُسجَّل، فلا تستطيع تعطيل الكتابات اللاحقة.

تُستخدم السمات أيضًا لاستهداف In-app. ملف التعريف على الخادم هو المرجع: لا يمكن أن تتجاوزه إلا عمليات الكتابة التي لم يؤكدها الخادم بعد، وهذا ما يمنع جهازين لجهة الاتصال نفسها من الاختلاف حول هويتها.

تسجيل الإشعارات والموافقة​

يسجّل SDK للإشعارات البعيدة سواء منح المستخدم إذن الإشعارات أم لا. في iOS الأمران منفصلان: يعطي التسجيل رمز جهاز دون موافقة، ولا يمكن لهذا الرمز إلا أن يوصل إشعارات في الخلفية (صامتة) - ولا يستطيع عرض أي شيء لم يأذن به المستخدم.

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

الرسائل داخل التطبيق​

اعرض رسائل داخل التطبيق موجّهة للمستخدمين بناءً على سلوكهم وسماتهم.

الاشتراك التسويقي​

ما إذا كان مسموحًا بمراسلة هذا الشخص على قناة ما. هذه موافقة مختلفة عن optOut() التي تخص ما إذا كان مسموحًا بتتبّعه - ولا تستلزم إحداهما الأخرى.

هذا ما يكتبه مركز التفضيلات داخل التطبيق.

Joryio.shared.setSubscription(channel: "email", status: "unsubscribed")
Joryio.shared.addToSubscriptionGroup("list_abc123", channel: "email")

تغيير الاشتراك الذي يحدث وموافقة التتبع معلّقة ينتظر على الجهاز ويُرسل عند منح الموافقة. والتغيير الذي يفشل يُحفظ، عبر إعادة التشغيل وreset()، ويُرسل مجددًا - راجع عمليات الكتابة التي يجب أن تصل إلى الخادم.

إلغاء الاشتراك - status: "unsubscribed" أو removeFromSubscriptionGroup - هو سحب للموافقة: يُقبل ويُسلَّم حتى إن ألغى الشخص الاشتراك في التتبع، أو كانت الموافقة معلّقة أو مرفوضة، أو بعد wipeData(). أما الاشتراك والانضمام إلى مجموعة فلا.

القنوات: email, sms, whatsapp, push, viber. الحالات: optedIn, subscribed, unsubscribed.

المعرّف مطلوب - تُتخطّى المكالمة، مع تسجيل خطأ، إن لم يوجد معرّف مستخدم ولا معرّف مجهول.