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

دمج Unity SDK

يغلّف Unity SDK حزمتَي Joryio الأصليتين لنظامي iOS وAndroid. وهو لا يضيف منطق تتبّع خاصاً به: كل استدعاء يُمرَّر إلى الحزمة الأصلية، فيكون السلوك والتجميع والتعامل مع وضع عدم الاتصال مطابقاً لدمج أصلي تماماً.

المتطلبات​

  • Unity 2022.3 LTS أو أحدث. يبني Unity 2021.3 و2022.1 تطبيقات Android باستخدام JDK 8 ولا يمكنهما الترجمة مقابل SDK Android الأصلي (bytecode بإصدار Java 11).
  • iOS 14.0+ / Android 6.0+ (API 23)
  • Android: مستوى Target API Level 34 أو أعلى (في Player Settings). تتطلب مكتبات AndroidX في SDK الأصلي compileSdk 34، ويفشل بناء Android في Unity بأقل من ذلك.
  • Android: ‏External Dependency Manager for Unity (EDM4U). لا يستخدمه iOS (انظر التثبيت).
  • iOS: ‏Xcode 15 أو أحدث، مع اتصال شبكي بـ github.com عند أول بناء للمشروع المصدَّر - إذ يُنزّل Xcode الـ SDK الأصلي كحزمة Swift.
  • لإشعارات Android: Firebase Cloud Messaging داخل اللعبة - حزمة Messaging من Firebase Unity SDK وملف google-services.json الخاص بك. تُبنى حزمة Android الأصلية مقابل firebase-messaging لكنها لا تجلبها؛ ومن دونها تعمل التحليلات والرسائل داخل التطبيق وتبقى الإشعارات معطّلة.
المنصات المدعومة

iOS وAndroid فقط. على المنصات الأخرى - المحرّر وسطح المكتب وأجهزة الكونسول - توجد كل الدوال ولا تفعل شيئاً، ويسجّل SDK تحذيراً واحداً لكل جلسة. ولا يحتاج كود لعبتك أبداً إلى #if UNITY_IOS || UNITY_ANDROID حول استدعاء Joryio.

التثبيت​

أضف الحزمة (com.joryio.sdk) من Git: في Package Manager اختر + → Add package from git URL وأدخل العنوان مع وسم الإصدار:

https://github.com/joryio/joryio-unity-sdk.git#1.3.3

للتحديث، غيّر الوسم إلى الإصدار الجديد. ثبّت وسماً دائماً: بدونه يتتبع Package Manager أحدث commit.

أو نزّل joryio-unity-<version>.unitypackage من صفحة الإصدارات على GitHub واستورده عبر Assets → Import Package → Custom Package. يُثبَّت الـ SDK تحت Assets/JoryioSDK؛ للتحديث احذف هذا المجلد واستورد الإصدار الجديد.

استخدم طريقة تثبيت واحدة، لا الاثنتين. حزمة Git URL والمجلد Assets/JoryioSDK يحملان الـ assemblies نفسها (Joryio.Runtime وJoryio.Editor) ومعرّفات GUID نفسها للأصول، لذا لا يُترجَم مشروع يحتوي على الاثنتين. للتبديل، أزل الطريقة المثبّتة أولاً: احذف Assets/JoryioSDK، أو أزل حزمة Joryio من Package Manager. ويجلب ملف .unitypackage أيضاً شروط الترخيص (LICENSE) وREADME.md وCHANGELOG.md إلى Assets/JoryioSDK.

الحزمة غير متوفرة على OpenUPM، الذي يقبل الحزم مفتوحة المصدر فقط؛ حزم SDK من Joryio مرخّصة تجارياً.

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

  • Android: دع EDM4U يحلّ io.joryio:joryio-android عبر Gradle، من بيان تبعيات الحزمة (Editor/JoryioDependencies.xml).
  • iOS: لا شيء يُحلّ داخل Unity. عند البناء لـ iOS تضيف الحزمة الـ SDK الأصلي إلى مشروع Xcode المصدَّر كحزمة Swift (https://github.com/joryio/joryio-ios-sdk، بإصدار الحزمة نفسه تماماً) وتربط المنتج Joryio بـ UnityFramework. يُنزّلها Xcode ويحلّها في أول مرة يفتح فيها المشروع أو يبنيه. لا يشارك CocoaPods ولا EDM4U على iOS.
الترقية من إصدار استخدم CocoaPods

كانت الإصدارات السابقة تأخذ iOS SDK من CocoaPods (الحزمة Joryio/Core)، ويتوقف CocoaPods trunk عن قبول إصدارات جديدة في 2 ديسمبر 2026. بعد الترقية، ابنِ مشغّل iOS مرة واحدة بخيار Replace بدلاً من Append، كي لا يحمل المشروع المصدَّر الـ pod القديم بعد الآن، واحذف أي سطر <iosPod name="Joryio/..."> من ملفات تبعيات EDM4U الخاصة بك.

بداية سريعة​

لكل منصة مفتاح SDK خاص بها (jry_sdk_ios_...، jry_sdk_android_...)، من Settings → Apps في لوحة التحكم. استخدم عنوان API الظاهر لمساحة عملك.

using Joryio.Runtime;

void Awake()
{
#if UNITY_IOS
const string sdkKey = "jry_sdk_ios_YOUR_IOS_KEY";
#else
const string sdkKey = "jry_sdk_android_YOUR_ANDROID_KEY";
#endif
JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com");
}

ثم تتبّع ما يفعله اللاعبون:

JoryioSDK.Track("level_complete", new Dictionary<string, object> {
{ "level", 7 },
{ "duration_seconds", 143.5 },
});

JoryioSDK.Identify("player-1234");
JoryioSDK.SetAttribute("favourite_class", "mage");
JoryioSDK.IncrementAttribute("games_played");

// Another identifier for the same player - works before Identify too
JoryioSDK.AddAlias("steam_id", "76561198000000000");

تحتفظ القيم بأنواعها عبر الحدود: { "level", 7 } تُخزَّن كالرقم 7 لا كـ "7". والشرائح تقارن رقمياً، فالرقم المحوَّل إلى نص لن يطابق أحداً. كل نوع رقمي يُحفظ، والقواميس والقوائم المتداخلة تبقى كائنات ومصفوفات، ويُرسل DateTime بصيغة ISO-8601 بتوقيت UTC، ويصبح NaN أو اللانهاية null.

أما كقيمة سمة (SetAttribute وSetAttributes وIncrementAttribute) فيُتجاهَل NaN أو اللانهاية مع تحذير وتبقى السمة دون تغيير - ولا تُحذف. التحويل إلى null يقتصر على خصائص الحدث.

لا يرمي أي استدعاء استثناءً داخل لعبتك: الاستدعاء قبل Initialize، أو وسيط إلزامي قيمته null، أو فشل أصلي، يُسجَّل ويُتجاهل.

الاستثناء هو الموافقة: تعمل OptOut() وOptIn() وUpdateConsent() حتى قبل Initialize. يُطبَّق القرار عند بدء SDK، قبل إرسال أي شيء. ويجيب IsUserOptedOut() أيضًا قبل Initialize - انظر الموافقة. وتعمل WipeData() وGetTrackingConsent() قبل Initialize أيضًا، وكذلك SetSdkAuthenticationToken وSetSdkAuthenticationErrorHandler - انظر مصادقة SDK. وكذلك تغييرات الاشتراك وUnregisterPush(): يُحتفظ على الجهاز باستدعاءات SetSubscription وAddToSubscriptionGroup وRemoveFromSubscriptionGroup وUnregisterPush() التي تجري قبل Initialize، حتى بعد إعادة التشغيل، وتُرسل عند بدء SDK. والتغيير الأحدث للقناة نفسها أو للمجموعة نفسها يحل محل الأقدم، فيُرسل آخر اختيار للاعب. أما السحب (إلغاء اشتراك، أو مغادرة مجموعة، أو UnregisterPush()) فيُرسل دائمًا: يتجاهل OptOut() أو موافقة Denied أو WipeData() قبل Initialize الاشتراكات المحتفظ بها، ولا يتجاهل السحب أبدًا، والسحب الذي جرى قبل WipeData() يُرسل باسم اللاعب الذي طلبه، لا باسم الهوية الجديدة التي ينشئها المسح. ولا يُرسل RegisterPushToken قبل Initialize، ولا يلغي UnregisterPush() المحتفظ به. أما الاشتراك أو الموافقة (opt-in) أو الانضمام إلى مجموعة قبل Initialize بينما اللاعب قد ألغى الاشتراك في التتبّع (opt-out) أو الموافقة Denied (محفوظة من تشغيل سابق أو مضبوطة في هذا التشغيل)، فلا يُحتفظ به إطلاقاً: يُسقَط مع تحذير، ولا يرسله OptIn() أو Granted لاحقاً.

القيمة null لسمة ليست خطأً: SetAttribute("clan", null) يحذف السمة، تمامًا مثل UnsetAttribute("clan")، على iOS وAndroid على حد سواء.

الإعدادات​

كل إعداد اختياري ويقابل الخيار الذي يحمل الاسم نفسه في SDK الأصلي. المدد بالثواني والاسم يقول ذلك، لأن الـ SDK الأصليين يختلفان (iOS يأخذ ثواني وAndroid مللي ثانية) - الجسور تحوّل.

JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com", new JoryioConfig {
Debug = UnityEngine.Debug.isDebugBuild,
FlushIntervalSeconds = 10,
AnonymousId = existingDeviceId, // only if you already have one
});
الخاصيةالنوعالافتراضيالوصف
Debugboolfalseسجلات أصلية مفصّلة. يتبعها مستوى السجل الأصلي: debug عندما تكون true، والأخطاء فقط خلاف ذلك.
BatchSizeint50الأحداث في كل دفعة قبل التفريغ التلقائي.
FlushIntervalSecondsdouble5الثواني بين عمليات التفريغ التلقائي.
SendImmediatelyboolfalseإرسال كل حدث فور تتبعه دون تجميع في دفعات، غير موصى به للإنتاج.
MaxQueueSizeint1000أقصى عدد أحداث تحتفظ به قائمة الانتظار المحلية أثناء فشل الإرسال؛ بعده تُحذف الأقدم.
RequestTimeoutSecondsdouble10مهلة HTTP لكل طلب.
SessionTimeoutSecondsdouble1800ثواني الخمول قبل بدء جلسة جديدة.
TrackSessionStartbooltrueإرسال حدث بدء الجلسة تلقائيًا.
AnonymousIdstringيُولَّد تلقائيًامعرّفك المجهول الخاص لهذا الجهاز. يُستخدم لهذا التشغيل ويُخزَّن فقط عندما لا يوجد شيء مخزَّن بعد، فيبقى المعرّف المضبوط مرة واحدة حتى لو توقفت اللعبة عن تمرير الخيار.
RequestPushPermissionAtLaunchboolfalseطلب إذن الإشعارات أثناء Initialize. انظر أدناه لمعرفة سبب إيقافه افتراضيًا.
MaxRetriesint3عدد إعادات المحاولة لكل طلب قبل أن ينتظر التفريغ التالي.
RetryBackoffMsint1000التأخير الأساسي بين إعادات المحاولة، بالمللي ثانية (اسم الـ SDK الأصليين).
TrackingConsentTrackingConsent?null (غير مضبوط)إجابة شاشة الموافقة لديك عند التشغيل. إن لم يُضبط تبقى الإجابة التي يتذكرها الـ SDK (فـ Denied المخزّن يبقى رفضًا)، ومن دون إجابة مخزّنة يجري التتبع كالمعتاد. Granted يتتبّع ويستبدل الرفض المخزّن؛ وPending لا يرسل شيئًا حتى UpdateConsent(TrackingConsent.Granted)، لهذا التشغيل فقط؛ وDenied لا يجمع شيئًا ويُتذكَّر. انظر الموافقة.
OptOutboolfalseالبدء في وضع إلغاء الاشتراك في التتبّع. يُخزَّن هذا الوضع، لذا لا تُلغي القيمة false استدعاء OptOut() سابقًا.
NotificationChannelNamestringnull ("Notifications")Android فقط. اسم قناة الإشعارات في إعدادات الإشعارات بالنظام (Android 8 فأحدث)؛ مرّره مترجمًا. لا قنوات في iOS فيتجاهله.
EnableSdkAuthenticationboolfalseإرسال رمز موقَّع مع كل طلب. انظر مصادقة SDK.
SdkAuthenticationTokenstringلا شيءالرمز الذي تبدأ به إن كان لديك واحد.
AllowHtmlJsInAppMessagesboolfalseيسمح للواجهة الأصلية برسم رسائل HTML. لا تعمل السكربتات داخلها؛ ويتولى جسر الحزمة الأصلية الأزرار والنماذج. لا أثر له على الرسائل التي يرسمها معالجك.
HoldInAppMessagesForHandlerboolfalseمنذ الإصدار 1.3.2. لعبتك ترسم رسائل In-App بمعالجها الخاص: تُحتجز الرسائل الجاهزة قبل استدعاء SetInAppMessageHandler. انظر قبل تعيين المعالج.

يضبط Debug مستوى السجل الأصلي ووضع التصحيح الأصلي معًا على iOS وAndroid، وهذا يفتح أيضًا استدعاءَي الاختبار ResetDisplayedCampaigns وEvaluateInAppCampaigns.

الإشعارات الفورية​

إن لم تكن لعبتك تملك منظومة إشعارات بعد، يسجّل الـ SDK الجهاز بنفسه - على iOS دائمًا، وعلى Android حين يكون Firebase Cloud Messaging موجودًا في اللعبة (انظر المتطلبات). ومن دون Firebase على Android لا يوجد رمز FCM، فلا يُسجَّل شيء وتبقى الإشعارات معطّلة؛ ولا تتأثر التحليلات والرسائل داخل التطبيق. اطلب الإذن حين يكون لدى اللاعب سبب للموافقة: الطلب في الثواني الأولى من اللعب هو الأكثر رفضاً، والرفض أصعب بكثير في التراجع عنه من التأجيل.

JoryioSDK.RequestPushPermission(granted => {
Debug.Log(granted ? "Push enabled" : "Player declined");
});

يُنفَّذ الـ callback على الخيط الرئيسي مرة واحدة بالضبط. وعلى iOS يتلقى SDK بعد ذلك رمز APNs بنفسه - لا يوجد app delegate لتعديله.

وعلى Android يظهر الطلب من نشاط اللعبة: GameActivity (الافتراضي في Unity 6) أو UnityPlayerActivity الكلاسيكي (الإصدارات الأقدم، أو مشروع Unity 6 يختاره). وفي الحالتين تُبلَّغ إجابة اللاعب بعد إغلاق نافذة النظام. وقبل Android 13 لا يوجد طلب إذن وقت التشغيل، ويجيب الـ callback فورًا بما إذا كانت الإشعارات مفعّلة.

وإن فضّلت أن يطلبه SDK أثناء Initialize - في إصدار تجريبي محدود، أو لعبة تكون الإشعارات جوهرها، أو لمجرد رؤية التدفّق - فعّله في الإعداد:

JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com", new JoryioConfig {
RequestPushPermissionAtLaunch = true,
});

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

إن كنت تستخدم Firebase أو Unity Mobile Notifications بالفعل​

معظم الألعاب كذلك، وتنافس منظومتَي إشعارات على الرمز نفسه هو الطريقة المعتادة لانهيار الدمج. أبقِ منظومتك وسلّمنا الرمز - رمز جهاز APNs على iOS، ورمز تسجيل FCM على Android. على Android يأتي هذا الرمز من Firebase Messaging (أو من غلاف FCM خاص بك): لا تُنتج Unity Mobile Notifications رمز FCM على Android - فهي تجدول إشعارات محلية هناك - لذا لا يمكنها تقديم الرمز إلا على iOS:

JoryioSDK.RegisterPushToken(tokenFromYourExistingStack);

أضف callback لمعرفة النتيجة - isRetryable يعني عطلًا مؤقتًا (بلا اتصال، DNS، 5xx) يستحق التسجيل مجددًا، وisUnauthorized يعني أن مفتاح SDK خاطئ:

JoryioSDK.RegisterPushToken(tokenFromYourExistingStack, result =>
{
if (!result.success && result.isRetryable)
{
// عطل مؤقت (بلا اتصال، DNS، 5xx): سجّل مرة أخرى عند عودة الشبكة
}
});

على Android لا يمسّ SDK عندها خدمة FCM. أما على iOS فيحدث أمران، حتى لا تُفاجأ:

  • يجعل Initialize الـ SDK مفوَّضًا (delegate) لـ UNUserNotificationCenter، ويمرّر كل callback إلى المفوَّض الذي كان مثبّتًا قبله. هيّئ Joryio بعد أن يضبط ملحق الإشعارات الآخر مفوَّضه (يفعل Unity Mobile Notifications ذلك عند الإقلاع، لذا Awake مناسب). والملحق الذي يضبط المفوَّض لاحقًا يحلّ محل مفوَّض Joryio، وعندها يعتمد تتبّع الفتح على استدعائك لـ TrackPushClick.
  • يتلقى SDK رمز APNs من app controller الخاص بـ Unity بنفسه. وتمرير الرمز نفسه إلى RegisterPushToken أيضًا لا يضر - إلا إذا أوقف اللاعب الإشعارات، لأن RegisterPushToken يلغي UnregisterPush() (انظر أدناه).

يعيد RegisterPushToken تشغيل الإشعارات لهذا الجهاز: فهو يلغي UnregisterPush() سابقًا. ويُستدعى OnTokenReceived في Firebase من تلقاء نفسه عند كل تشغيل، لذا إن كنت تمرّره فتجاهله ما دام اللاعب قد أوقف الإشعارات في إعداداتك، واستدعِ RegisterPushToken عندما يعيد تشغيلها. وإلا فإن تحديث الرمز، لا اللاعب، هو ما يلغي اختياره.

يضبط UpdateBadgeCount(count) وClearBadge() الشارة على أيقونة التطبيق في iOS، ولا يفعلان شيئًا على Android الذي لا يملك واجهة برمجة للشارات.

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

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

اعرضها داخل المحرّك برسومك أنت (الخيار الموصى به للألعاب) - ثبّت معالجاً:

JoryioSDK.SetInAppMessageHandler(message => {
// Title / Body / ImageUrl / Buttons are the authored content.
// Name is the campaign's dashboard label - never copy to show a player.
myCanvas.Show(message.Title, message.Body, message.ImageUrl);
foreach (var button in message.Buttons)
myCanvas.AddButton(button.Text, () => {
message.MarkClicked(button);
if (button.Action == "url" || button.Action == "deep_link")
Application.OpenURL(button.Url);
else if (button.Action == "request_push_permission")
JoryioSDK.RequestPushPermission(granted => { });
myCanvas.Hide();
message.MarkDismissed(); // the tap closed the message
});
message.MarkDisplayed();
}, canRenderHtml: false);
أنت الآن مسؤول عن الإبلاغ

تثبيت معالج يوقف الـ SDK عن الرسم، ومن تلك اللحظة لا يستطيع رؤية ما حدث. استدعِ MarkDisplayed() عند ظهور الرسالة، وMarkClicked(button) / MarkDismissed() حسب تصرف اللاعب. يُبلغ MarkDisplayed() مرة واحدة لكل رسالة مهما تكرر استدعاؤه، ويرسل MarkClicked(button) قيمة Id للزر بوصفها elementId للنقرة، لتميّز خريطة النقرات بين الأزرار. الزر بلا معرّف لا يرسل elementId - ولا يرسل نصه أبدًا، فهو نص عرض.

النقرة التي تغلق الرسالة يُبلَّغ عنها بالأمرين معًا: MarkClicked(button) ثم MarkDismissed()، كما ترسل الحزم الأصلية حين ترسم الرسالة بنفسها.

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

لإعادة الرسم إلى الحزمة الأصلية - عند تدمير المُصيّر الخاص بك، أو في مشهد بلا واجهة للرسم عليها - أزل المعالج:

JoryioSDK.ClearInAppMessageHandler(); // مثل SetInAppMessageHandler(null)

ترسم الحزمة الأصلية حينها الرسائل بعروضها الخاصة مجددًا (إن كانت مكونات الواجهة الخاصة بها ضمن البناء - انظر أدناه) وتُبلغ الخادم بقدراتها. هذا هو clearInAppMessageCallback في الحزم الأصلية.

من مكوّن قد لا يكون العارض الوحيد - عارض لكل مشهد مثلًا - مرّر المعالج الذي ثبّتّه. لا يُزال إلا إن كان لا يزال هو المثبّت، ولا يُزال أبدًا معالج ثبّته سكربت آخر بعده:

void OnDestroy() => JoryioSDK.ClearInAppMessageHandler(myHandler);

يصف canRenderHtml مُصيّرك أنت. فلوحة Unity العادية لا تعرض محتوى HTML، وهو يحتاج عرض ويب. وهذا ليس أمراً شكلياً: تُستهدف الحملات بناءً على ما يُبلغ به الجهاز، والقدرة غير المعلنة تُقرأ على أنها "لا يستطيع" لا "غير معروف". فادّعاء HTML لا تستطيع رسمه يرسل حملات إلى لاعبين لن يروها أبداً، وإغفال HTML تستطيع رسمه يحرمك من حملات كان بإمكانك عرضها.

إن كان عرض الويب لديك يعرض رسالة HTML فيها <form data-jry-form>، فأرسلها عبر SDK لتُكتب القيم في ملف اللاعب. القيم نصوص، أو قائمة نصوص لمجموعة خانات اختيار:

JoryioSDK.SubmitInAppForm(message.Id, new Dictionary<string, object> {
{ "email", "[email protected]" },
{ "topics", new List<string> { "events", "offers" } },
}, result => {
if (!result.ok) Debug.Log(result.message);
});

يُنفَّذ الـ callback دائمًا مرة واحدة؛ وأي فشل، بما في ذلك انقطاع الشبكة، يكون ok = false.

قبل تعيين المعالج​

منذ الإصدار 1.3.2. لا يمكن استدعاء SetInAppMessageHandler إلا بعد Initialize، وقد تنتهي المزامنة الأولى قبله - فترسم الحزمة الأصلية تلك الرسالة بنفسها إن كانت واجهتها ضمن البناء. اضبط HoldInAppMessagesForHandler = true في JoryioConfig لمنع ذلك: الرسالة الجاهزة قبل تعيين معالجك تُحتجز - لا تُرسم، ولا يُبلغ عنها، ولا تُحتسب ضمن أي حد للتكرار - وتُسلَّم إلى معالجك لحظة تعيينه. أما ClearInAppMessageHandler() بدلًا من ذلك فينهي الاحتجاز وترسم الحزمة الأصلية الرسالة المحتجزة.

أو دع الحزمة الأصلية ترسمها​

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

Android: أضف حزمة الواجهة في ملف تبعيات EDM4U خاص بك، مثل Assets/Editor/JoryioUIDependencies.xml، بإصدار مطابق لإصدار الحزمة:

<dependencies>
<androidPackages>
<androidPackage spec="io.joryio:joryio-android-ui:1.3.3" />
</androidPackages>
</dependencies>

iOS: أضف رمز التعريف (scripting define symbol) ‏JORYIO_IOS_UI ضمن Player Settings → iOS → Other Settings → Scripting Define Symbols. عندها يربط بناء iOS أيضاً المنتج JoryioUI من حزمة Swift، بإصدار الحزمة.

المعالج المثبّت عبر SetInAppMessageHandler يظل يتولّى العرض بدلاً من الواجهات الأصلية.

ما تحمله الرسالة​

يتلقّى المعالج كل ما أرسله الخادم، وبأسماء الحقول نفسها التي يستخدمها SDK الخاص بـ React Native (كان يتلقّى سابقًا Id وName فقط، فلم يبقَ للّعبة ما تعرضه سوى التسمية الداخلية للحملة):

الحقليُضبط عندماالمعنى
Id، Name، Typeدائمًامعرّف الحملة، واسمها في لوحة التحكم (ليس نصًا للعرض)، وmodal / banner / slideup / fullscreen / custom.
Kindدائمًاnative أو html - أي المجموعتين أدناه مملوءة. IsNative / IsHtml اختصاران.
Title، Body، ImageUrlnativeالنص والصورة المؤلَّفان. قد يكون Title وImageUrl فارغين (null)؛ أما Body فمضبوط دائمًا.
Buttonsnativeليس null أبدًا. لكل زر Id وText وAction (dismiss، url، deep_link، request_push_permission) وUrl لإجراءي الرابط. لا يستطيع الـ SDK التصرف بناءً على زر ترسمه أنت: افتح الرابط بنفسك، واستدعِ RequestPushPermission لإجراء التمهيد، وأبلغ عن النقرة بـ MarkClicked(button).
CloseButton، BackdropDismissiblenativeهل طلب المؤلّف زر إغلاق، وهل النقر خارج الرسالة يغلقها.
Stylenativeتجاوزات العرض التي حددها المؤلّف، أو null إن لم تحدد الحملة أيًا منها. BackgroundColor وTextColor وPrimaryButtonColor وPrimaryButtonTextColor (سلاسل ألوان CSS)، وCornerRadius، وFontSize (حجم النص الأساسي - اشتق منه حجم العنوان)، وTitleWeight (regular / medium / semibold / bold)، وTextAlign (auto / start / center / end - يتبع auto لغة الرسالة نفسها لا إعدادات الجهاز)، وFontFamily. كل حقل اختياري، وغياب الحقل يعني وراثة تنسيق لعبتك؛ طبّق الموجود منها ولا تستبدل الباقي بافتراضيات من عندك. FontFamily بذل جهد فقط - طبّقه فقط إن كانت لعبتك تتضمن هذا الخط. الحقول والأسماء نفسها كما في message.style في React Native SDK.
Html، Csshtmlالمستند. لا يمكن لكانفس uGUI عرضه - لا تعلن canRenderHtml: true إلا إذا كانت لعبتك تستضيف web view.

ما لا يستطيع Unity عرضه، بوضوح. يحتاج محتوى HTML إلى web view لا يوفّره المحرّك. الصور عناوين URL: نزّلها بنفسك (UnityWebRequestTexture). تصل تجاوزات التنسيق التي يحددها المؤلّف في Style، لكن لا شيء يطبّقها عنك - لا يملك الـcanvas سمة يرثها، لذا يقرأ عارضك الحقول التي يدعمها ويحتفظ بتنسيقه للباقي.

ابدأ من العيّنة​

تتضمن الحزمة عيّنة In-app renderer: مشهد، وprefab قابل لإعادة التصميم، والسكربت الذي يربط بينهما. استوردها من Package Manager (اختر حزمة Joryio، وافتح Samples واضغط Import)، ثم ضع مفاتيح SDK الخاصة بك على الكائن JoryioInApp في JoryioInAppSample.unity، وابنِ لجهاز iOS أو Android. تصل الرسائل فقط في بناء للجهاز: في الـ Editor لا يفعل الـ SDK شيئًا، لذا الضغط على Play لا يعرض شيئًا.

ثبّتّ من ملف .unitypackage؟ لا يوجد إدخال في Package Manager للاستيراد منه: نزّل joryio-unity-<version>-sample.unitypackage من صفحة الإصدارات نفسها واستورده بالطريقة نفسها. يُثبَّت في Assets/Samples/Joryio/<version>/In-app renderer، حيث يضع Package Manager العيّنة، ويحتاج أيضاً إلى com.unity.ugui.

الـ prefab هو الجزء الذي تجعله خاصًا بك - غيّر الألوان والخط وأضف رسوماتك. يرسم المُصيّر ما في حقل Panel Prefab الخاص به (JoryioInAppPanel.prefab في مشهد العيّنة) ويملأ أربعة أبناء يجدها بالاسم: Title وBody (Text)، وPrimaryButton (يُنسخ مرة لكل زر في الحملة)، وSecondaryButton (زر الإغلاق). تُطبَّق تجاوزات التصميم التي وضعها المؤلف فوقه حيث حددتها الحملة. وإذا كان الحقل فارغًا، يبني لوحة بسيطة بالكود.

تحتاج أزرار uGUI إلى EventSystem، لذا ينشئ المُصيّر واحدًا عندما يخلو المشهد منه - مع وحدة الواجهة الخاصة بـ Input System عندما تكون هي طريقة الإدخال النشطة لديك، وإلا مع الوحدة القديمة. يوضح السكربت الشيء الوحيد الذي يجب الإبقاء عليه: الإبلاغ عن دورة حياة كل رسالة. تتطلب العيّنة com.unity.ugui، وهو ما لا يتطلبه الـ SDK نفسه.

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

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

  • الرسائل التي يرسمها الـ SDK الأصلي تتبع قاعدة iOS وAndroid: تُعرض فقط على شاشة مستقرة في الواجهة، وتُحتسب بعد ثانية واحدة على الشاشة والتطبيق في الواجهة. النقر أو الإغلاق قبل ذلك يُحتسب مشاهدة؛ والرسالة التي تغادر الشاشة أبكر لا تُحتسب وتبقى مؤهلة.
  • إذا كانت اللعبة ترسم الرسائل بنفسها، استدعِ MarkDisplayed() وفق القاعدة نفسها - JoryioSDK.IN_APP_DISPLAY_DWELL_MS: بعد أن تبقى الرسالة ظاهرة طوال هذه المدة، أو مباشرة قبل MarkClicked / MarkDismissed عندما يتفاعل اللاعب أبكر. لا تستدعه لرسالة غادرت الشاشة قبل ذلك (تغيير المشهد، إيقاف التطبيق مؤقتًا).

مفتاحان مستقلان، متطابقان في كل حزم Joryio:

  • موافقة التتبع - إجابة شاشة الموافقة لديك: Granted أو Denied أو Pending.
  • إلغاء الاشتراك (opt-out) - اختيار اللاعب نفسه: OptOut() / OptIn().

لا تُجمع البيانات إلا حين تُمنح الموافقة ولم يُلغِ اللاعب اشتراكه؛ وأي "لا" هي الغالبة. ولا يغيّر أي مفتاح الآخر: UpdateConsent(TrackingConsent.Granted) لا يرفع إلغاء الاشتراك (وحده OptIn() يرفعه)، وOptOut() لا يغيّر الموافقة.

إن لم يُجب اللاعب بعد على شاشة الموافقة لديك، فابدأ بموافقة Pending: لا يُرسل شيء - لا بدء جلسة، ولا مزامنة للرسائل داخل التطبيق، ولا تسجيل للإشعارات - وتنتظر الأحداث على الجهاز (حتى MaxQueueSize) إلى أن تصل الإجابة.

JoryioSDK.Initialize(sdkKey, apiHost, new JoryioConfig {
TrackingConsent = TrackingConsent.Pending,
});
// عندما يجيب اللاعب
JoryioSDK.UpdateConsent(TrackingConsent.Granted); // يرسل ما كان منتظرًا
// أو
JoryioSDK.UpdateConsent(TrackingConsent.Denied); // يحذفه ولا يجمع شيئًا
  • Denied يُتذكَّر عبر إعادة التشغيل. ولا يُرفع إلا بـ UpdateConsent(TrackingConsent.Granted) أو بضبط صريح TrackingConsent = TrackingConsent.Granted في الإعدادات.
  • ترك JoryioConfig.TrackingConsent دون ضبط (الافتراضي) يُبقي الرفض المخزّن؛ ومن دون إجابة مخزّنة يتتبّع الـ SDK كالمعتاد، فاللعبة التي لا شاشة موافقة فيها لا تحتاج إلى أي إعداد.
  • Pending يسري على التشغيل الحالي فقط، ولا يتغلّب أبدًا على Denied مخزّن.
  • القيمة الخارجة عن الـ enum (تحويل خاطئ) تُتجاهَل مع تحذير؛ وفي الإعدادات تُعامَل كأنها غير مضبوطة.
JoryioSDK.OptOut(); // إيقاف الجمع، وتُهمَل الأحداث المنتظرة وعمليات كتابة الخصائص المعلّقة
JoryioSDK.OptIn(); // استئناف
JoryioSDK.WipeData(); // محو كل ما هو مخزّن على الجهاز (يبقى إلغاء الاشتراك)

يُخزَّن إلغاء الاشتراك ويبقى بعد إعادة التشغيل. ولبدء جلسة في وضع إلغاء الاشتراك مباشرة، اضبط JoryioConfig.OptOut = true.

يعكس IsUserOptedOut() مفتاح إلغاء الاشتراك وحده، لا الموافقة. ويُرجع GetTrackingConsent() مفتاح الموافقة: Granted أو Denied أو Pending. ويجيب كلاهما قبل Initialize أيضًا مما هو مخزّن على الجهاز: يُبلَّغ عن إلغاء اشتراك أو Denied محفوظ من تشغيل سابق كما هو، لا كقيمة افتراضية، ويتقدّم OptOut() / OptIn() المستدعى في التشغيل نفسه على إلغاء الاشتراك المخزّن. ولا يُرجع GetTrackingConsent() القيمة Granted إلا عندما لم يُتّخذ أي قرار، وهكذا يتصرف SDK أيضًا في هذه الحالة.

عندما يتغير إلغاء الاشتراك، يسجّله SDK في ملف اللاعب كسمة منطقية tracking_opted_out، لكي تستثني الشرائح اللاعبين الذين ألغوا الاشتراك. ويُبلغ GetTrackingConsent() عن الموافقة التي يحتفظ بها SDK الأصلي، بما فيها Denied محفوظة من تشغيل سابق.

مصادقة SDK​

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

في مساحات العمل التي فُعّلت فيها مصادقة SDK، يحمل كل طلب رمزًا يوقّعه خادمك:

JoryioSDK.Initialize(sdkKey, apiHost, new JoryioConfig { EnableSdkAuthentication = true });
JoryioSDK.SetSdkAuthenticationToken(tokenFromYourServer); // بعد تسجيل الدخول

JoryioSDK.SetSdkAuthenticationErrorHandler(error => {
// error.reason: missing أو invalid أو expired أو sub_mismatch أو unknown
// error.refreshExhausted: توقف SDK عن إعادة محاولة هذا الطلب
FetchFreshToken(token => JoryioSDK.SetSdkAuthenticationToken(token));
});

يعمل SetSdkAuthenticationToken وSetSdkAuthenticationErrorHandler قبل Initialize أيضًا. تبدأ SDK الأصلية بهذا الرمز قبل أول طلب لها، وتحتفظ به عبر WipeData() يُستدعى قبل Initialize، فيظل إلغاء الاشتراك المحتجز للاعب الذي تُمحى بياناته يُرسل برمزه. ويمكنك أيضًا تمرير الرمز الأول في JoryioConfig.SdkAuthenticationToken.

يُنفَّذ المعالج على الخيط الرئيسي. ويُستدعى عند 401 مصادقة على طلبات اللاعب الحالي، حتى قبل تعيين أي رمز (reason missing) - ولا يُستدعى أبدًا لبقايا لاعب سجّل خروجه، فلا يدفعك أبدًا إلى تسجيل خروج اللاعب الذي يلعب الآن. وتُحفظ أحداث اللاعب الحالي وعمليات الكتابة المرفوضة ويُعاد إرسالها بعد أن تعيّن رمزًا جديدًا. وتُرسل الأحداث في دفعات للاعب واحد في كل مرة، وتُحذف أحداث وعمليات كتابة اللاعب الذي سجّل خروجه عند أي 401 مصادقة (missing أو invalid أو expired أو sub_mismatch)، كي لا تؤخر أبدًا أحداث اللاعب التالي. ويُخرج Reset() الرمز من الجلسة؛ فيبقى مع بقايا اللاعب المغادر، ولا تحمله طلبات اللاعب التالي أبدًا - اضبط رمزه باستخدام SetSdkAuthenticationToken عند تسجيل دخوله. أما الأحداث وعمليات الكتابة التي وضعها اللاعب المغادر في قائمة الانتظار فتُرسل مع رمزه هو، فلا يضيع شيء عند تسجيل الخروج العادي.

التجارة الإلكترونية​

استخدم المتتبّع المُنمَّط بدل Track لأحداث التجارة. فهو يطبّق أسماء الأحداث القياسية التي تقرؤها تقارير الإيرادات والإسناد؛ أما الاسم المكتوب يدويًا مثل "Added to Cart" فيُخزَّن كحدث مختلف ولا يطابق أي تقرير.

var item = new EcommerceCartItem { ProductId = "gems_500", Name = "500 gems", Price = 4.99, Quantity = 1 };
JoryioSDK.Ecommerce.AddToCart(item, cartValue: 4.99);
JoryioSDK.Ecommerce.Purchase(new EcommerceOrder {
OrderId = "order-1042",
Items = new List<EcommerceCartItem> { item },
Value = 4.99,
Currency = "USD",
});

يملك المتتبّع الأحداث العشرين نفسها الموجودة في SDK الخاص بـ React Native، من ViewProduct إلى SubmitReview.

الاستدعاء الذي ينقصه وسيط مطلوب يسجّل تحذيرًا ولا يرسل شيئًا - لا حدثًا فارغًا ولا حدثًا بقيمة صفر أبدًا: منتج بلا ProductId، أو طلب بلا OrderId أو Items، أو معرّف طلب أو قسيمة فارغ، أو استدعاء سلة بلا قائمة العناصر. ويُرفض بالطريقة نفسها أي مبلغ قيمته NaN أو لانهائية (Value وPrice وcartValue وShipping وTax وDiscount وrefundAmount وdiscountAmount)، بدل تسجيله 0.

الهوية والجلسة​

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

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

var (userId, anonymousId) = JoryioSDK.GetIdentity(); // userId قيمته null للاعب المجهول
var (sessionId, sessionStart, duration) = JoryioSDK.GetSessionInfo(); // duration بالثواني

يُخرج Reset() اللاعب: معرّف مجهول جديد، ودون معرّف مستخدم.

مرجع الواجهة البرمجية​

كل عضو عام في JoryioSDK، بأسماء معاملاته. جميعها ثابتة (static) وآمنة قبل Initialize (تسجّل ولا تفعل شيئًا، ويظل الـ callback يجيب مرة واحدة).

الاستثناء: استدعاءات الموافقة وWipeData() واستدعاءات مصادقة SDK تسري فعلًا قبل Initialize، ويُحتفظ باستدعاءات الاشتراك وUnregisterPush() حتى استدعائه.

الدالةما تفعله
Initialize(sdkKey, apiHost, config = null)تشغيل الـ SDK. يُتجاهل الاستدعاء الثاني. ويُرفض مفتاح منصة أخرى (تبدأ مفاتيح iOS بـ jry_sdk_ios_ ومفاتيح Android بـ jry_sdk_android_) مع خطأ ويبقى الـ SDK متوقفًا. انظر الإعدادات.
IsInitializedهل نجح Initialize. تكون true أيضًا عند تشغيل iOS قبل أول فتح لقفل الجهاز: يُكمل الـ SDK الأصلي تشغيله عند فتح القفل، وتنتظره الاستدعاءات التي تجري في الأثناء. وإن كانت على الجهاز استدعاءات احتفظ بها تشغيل سابق، يُبقيها iOS غير قابلة للقراءة حتى فتح القفل ذاك أيضًا: فلا يجمع SDK شيئًا وينتظر كل استدعاء حتى تصبح قابلة للقراءة، ثم تُرسل أولًا وتليها الاستدعاءات التي جرت منذ ذلك الحين، بالترتيب. وفي الأثناء يُبلغ GetTrackingConsent() وIsUserOptedOut() عن القرارات التي اتخذها هذا التشغيل (الاستدعاءات وJoryioConfig). وإن لم يوجد قرار، يُبلغ GetTrackingConsent() عن Pending، لأن الموافقة المخزنة لا يمكن قراءتها بعد، ويجيب IsUserOptedOut() من SDK الأصلي، الذي قد لا يرى قبل فتح القفل إلغاء اشتراك (opt-out) خزّنه تشغيل سابق. وعند إرسال الاستدعاءات، تُطبَّق الموافقة التي حددها هذا التشغيل صراحةً (UpdateConsent قبل Initialize، وإلا JoryioConfig.TrackingConsent)؛ ومن دونها يُطبَّق القرار المحفوظ على الجهاز، ويُقرأ بعد فتح القفل، كما في حزم SDK الأصلية: تبقى Denied المخزنة Denied، وإن لم يُخزَّن قرار يكون SDK على Granted، كما في أي تشغيل آخر. أما دوال القراءة الأخرى، مثل GetUserId() وGetAnonymousId() وGetSessionId()، فتجيب من SDK الأصلي ولا تعكس بعد الاستدعاءات المنتظِرة.
SdkVersionإصدار الـ SDK، "1.3.3".
Track(eventName, properties = null)تتبّع حدث.
TrackScreen(screenName, properties = null)تتبّع مشاهدة شاشة.
Flush()إرسال الأحداث المنتظرة الآن.
Identify(userId)تعريف اللاعب. ما دام اللاعب قد ألغى التتبع أو كانت الموافقة Denied، يُحفظ المعرّف على الجهاز فقط (لا يُرسل شيء)، كي يصل إلغاء الاشتراك الذي يجري في الأثناء إلى اللاعب الصحيح.
Reset()تسجيل الخروج: معرّف مجهول جديد، بلا معرّف مستخدم.
AddAlias(label, name)إرفاق معرّف آخر، مثل ("steam_id", "7656..."). يعمل للاعب مجهول أيضًا.
GetUserId() / GetAnonymousId()المعرّفات الحالية ("" عند غيابها).
GetIdentity()(userId, anonymousId)؛ userId يساوي null للاعب المجهول.
SetAttributes(attributes)تعيين عدة سمات. القيمة null تحذف تلك السمة.
SetAttribute(key, value)تعيين سمة واحدة مع الحفاظ على نوعها. القيمة null تحذفها؛ وNaN أو اللانهاية تُتجاهَل.
IncrementAttribute(key, by = 1)الإضافة إلى سمة رقمية. NaN أو اللانهاية تُتجاهَل.
UnsetAttribute(key)حذف سمة.
UpdateConsent(consent)TrackingConsent.Granted أو Denied أو Pending. لا يغيّر إلغاء الاشتراك أبدًا. يعمل قبل Initialize أيضًا. انظر الموافقة.
OptOut() / OptIn()إيقاف الجمع أو استئنافه. يُحفظ عبر إعادة التشغيل؛ ولا يغيّر الموافقة أبدًا. يعملان قبل Initialize أيضًا.
IsUserOptedOut()هل ألغى اللاعب الاشتراك في التتبع (مفتاح إلغاء الاشتراك وحده). يجيب قبل Initialize أيضًا، من القرار المخزّن.
GetTrackingConsent()الموافقة الحالية: TrackingConsent.Granted أو Denied أو Pending (مفتاح الموافقة وحده). يجيب قبل Initialize أيضًا، من القرار المخزّن.
WipeData()محو كل ما خُزّن على الجهاز. يبقى إلغاء الاشتراك.
SetSubscription(channel, status)الاشتراك التسويقي لقناة واحدة. انظر الاشتراك التسويقي.
AddToSubscriptionGroup(groupId, channel) / RemoveFromSubscriptionGroup(groupId, channel)الانضمام إلى مجموعة اشتراك أو مغادرتها.
RequestPushPermission(onResult)طلب إذن الإشعارات؛ onResult(bool granted).
GetPushPermissionStatus(onResult)"granted" أو "denied" أو "notDetermined" أو "unavailable".
IsPushEnabled(onResult)هل الإشعارات مفعّلة؛ onResult(bool).
RegisterPushToken(token, onResult = null)تسليم رمز من منظومة الإشعارات الخاصة بك؛ onResult(PushRegistrationResult).
ReportPushDelivered(trackingId)الإبلاغ عن وصول إشعار (منظومة الإشعارات الخاصة بك).
TrackPushClick(trackingId)الإبلاغ عن فتح إشعار.
UnregisterPush()إيقاف الإشعارات لهذا الجهاز. يبقى متوقفًا حتى بعد إعادة التشغيل، إلى أن يُستدعى RegisterPushToken (بعد Initialize) أو يُطلب الإذن. ويُحتفظ به إذا استُدعي قبل Initialize.
GetDeviceToken()رمز الإشعارات الذي يحتفظ به الـ SDK ("" عند عدم وجوده).
UpdateBadgeCount(count) / ClearBadge()شارة أيقونة التطبيق على iOS؛ لا تفعل شيئًا على Android.
GetSessionId()معرّف الجلسة الحالية.
GetSessionInfo()(sessionId, sessionStart, duration)؛ المدة بالثواني.
SetSdkAuthenticationToken(token)تقديم رمز موقّع. انظر مصادقة SDK.
SetSdkAuthenticationErrorHandler(handler)التنبيه عند رفض طلب في ظل مصادقة SDK؛ القيمة null توقف الاستماع.
Ecommerceمتتبّع التجارة الإلكترونية المنمّط. انظر التجارة الإلكترونية.
SetInAppMessageHandler(handler, canRenderHtml = false)رسم رسائل in-app بنفسك. القيمة null تعيد الرسم إلى الحزمة الأصلية.
ClearInAppMessageHandler()إعادة رسم رسائل in-app إلى الحزمة الأصلية.
ClearInAppMessageHandler(handler)الأمر نفسه، فقط إن كان handler لا يزال هو المثبّت. وقيمة null لـ handler تزيل المثبّت أيًا كان.
TrackInAppImpression(campaignId, action, elementId = null)الإبلاغ عن displayed أو clicked أو dismissed؛ elementId هو معرّف الزر المنقور. يُفضَّل استخدام دوال Mark... في الرسالة.
SubmitInAppForm(campaignId, values, onResult)إرسال نموذج من رسالة HTML ترسمها أنت؛ onResult(InAppFormSubmitResponse).
SyncInAppCampaigns()جلب أحدث الحملات الآن.
EvaluateInAppCampaigns() / ResetDisplayedCampaigns()أدوات اختبار؛ تُتجاهل ما لم يكن Debug مفعّلًا.

على الرسالة التي يستلمها المعالج: MarkDisplayed() وMarkClicked(button = null) وMarkDismissed().

البناء​

يهيّئ SDK المشاريع المصدَّرة تلقائياً. لا شيء تفعله يدوياً:

  • Xcode: الـ SDK الأصلي كحزمة Swift ‏(joryio-ios-sdk، مربوطة بـ UnityFramework)، وإصدار Swift، وتضمين وقت تشغيل Swift، وقدرة الإشعارات، ووضع الخلفية للإشعارات البعيدة، وUNITY_USES_REMOTE_NOTIFICATIONS في Classes/Preprocessor.h - فبدونه يحذف Unity الـ callback الذي يسلّم رمز APNs. وتُضاف قدرة الإشعارات إلى ملف الـ entitlements الموجود للهدف إن وُجد، فتبقى entitlements الملحقات الأخرى.
  • Android: تُضاف قواعد keep لجسر Java إلى ملف proguard-unity.txt المولَّد، فيحتفظ بناء الإصدار المصغَّر بالدوال التي تستدعيها C# بأسمائها.

وإن لم تصل الإشعارات في بناء يعمل خلاف ذلك، تحقّق من وجود قدرة Push Notifications على الهدف: فهي الإعداد الوحيد الذي لا يُنتج غيابُه أي خطأ في أي مكان.

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

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

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

JoryioSDK.SetSubscription("email", "unsubscribed");
JoryioSDK.AddToSubscriptionGroup("list_abc123", "email");

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

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