كيفية تمرير معلمات التتبع من الويب إلى عمليات تثبيت التطبيق

opoinstall
2026-10-10
5 min read

كيفية تمرير معلمات التتبع من مواقع الويب على الجوال إلى عمليات التثبيت من المتجر؟ يتضمن تمرير معلمات التتبع من موقع ويب على الجوال إلى تطبيق تم تثبيته حديثاً تحميل حزمة OpoInstall Web JS SDK، والتقاط معلمات URL المدرجة في القائمة المسموحة، واستعادة بيانات الإسناد المدعومة من خلال حزمة SDK الأصلية عند تشغيل التطبيق لأول مرة.

ربط المعلمات عبر عمليات التنزيل من متجر التطبيقات هو عملية الحفاظ على معلمات URL الخاصة بالويب المؤهلة عندما ينتقل المستخدم من صفحة هبوط على الجوال إلى متجر التطبيقات ثم يقوم لاحقاً بتشغيل التطبيق المثبت حديثاً. تلتقط حزمة SDK الخاصة بالويب سياق الحملة أو الإحالة، بينما قد تجعل آلية مدعومة من المنصة أو خدمة الاستعادة المؤجلة المعلمات المرتبطة متاحة للتطبيق الأصلي. تعتمد الاستعادة على المنصة، وتكوين حزمة SDK، وإشارات المطابقة المتاحة، ومتطلبات الخصوصية المعمول بها.

المصطلح التعريف الكيان ذو الصلة دور نية البحث
معلمات التتبع أزواج المفتاح والقيمة المضافة إلى روابط URL لتحديد سياق الزيارات، أو مصادر الحملات، أو معلومات الإحالة. سلسلة استعلام URL إعلامي / تقني
تثبيت معلمات التطبيق سير عمل استعادة المعلمات المؤجلة الذي يجعل سياق الويب المؤهل متاحاً بعد التثبيت. حزمة SDK الأصلية للجوال تقني / إعلامي
Google Play Install Referrer واجهة برمجة تطبيقات (API) رسمية لنظام أندرويد لاسترجاع بيانات الإحالة الوصفية المرتبطة بعملية تثبيت من متجر Google Play. حزمة Android SDK تقني

فجوة إسناد الويب إلى المتجر: حواجز المنصات عبر متاجر التطبيقات

لا تعبر معلمات تتبع الويب تلقائياً حاجز التثبيت الخاص بمتجر التطبيقات.

حاجز الانفصال: كيف تعزل صناديق حماية نظام التشغيل سياق المتصفح

تستخدم تحليلات الويب عادةً سلاسل استعلام URL، بما في ذلك علامات UTM (utm_source، utm_medium، utm_campaign)، ومعرفات نقرات الإعلان مثل gclid وfbclid، ومعلمات إحالة مخصصة.

داخل موقع ويب مشارك، يمكن لواجهات برمجة تطبيقات المتصفح وأنظمة التحليلات الخاصة بالطرف الأول التقاط هذه القيم لسير عمل التحليلات والحملات المسموح بها.

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

وبالتالي، يحتاج المطورون إلى آلية إحالة مدعومة من المتجر أو سير عمل استعادة معلمات مؤجل لربط سياق ما قبل التثبيت المؤهل بجلسة التطبيق بعد التثبيت.

تفاصيل المنصات: متجر Apple App Store مقابل Google Play Store

يستخدم أندرويد وiOS آليات مختلفة قبل تطبيع المعلمات في عقد تطبيق واحد.

تدعم متاجر تطبيقات الجوال أساليب مختلفة لتمرير سياق الإحالة.

  • Apple App Store (iOS): لا تقوم عملية تثبيت قياسية من App Store بإعادة توجيه معلمات استعلام URL الخارجية التعسفية تلقائياً إلى بيئة التشغيل الأولى للتطبيق المثبت حديثاً. قد تساعد خدمات الاستعادة المؤجلة أو سير العمل المباشر الذي يتم بوساطة المستخدم في استعادة السياق المؤهل، وفقاً لمتطلبات الخصوصية الخاصة بشركة Apple.
  • Google Play Store (Android): توفر Google واجهة برمجة التطبيقات الرسمية Google Play Install Referrer API. حيث يتم استخدام سير عمل إحالة Google Play مبني بشكل صحيح، يمكن للتطبيق المثبت استرداد بيانات الإحالة الوصفية المدعومة، بما في ذلك سلسلة الإحالة والطوابع الزمنية ذات الصلة.

لا ينبغي التعامل مع هذه الآليات على أنها قابلة للتبديل. يوفر Google Play واجهة إحالة رسمية على مستوى المتجر، بينما تتطلب استعادة المعلمات المؤجلة التعسفية على iOS آلية مصممة بشكل منفصل ومتوافقة مع السياسات.

الأثر التجاري لحركة تثبيت غير مسندة على حساب عائد الاستثمار للقنوات

إذا فُقدت معلمات الحملة بين زيارة الويب وتثبيت التطبيق، فقد لا تقوم أنظمة التحليلات بربط عملية التثبيت بشكل صحيح بحملتها الأصلية.

قد يؤدي هذا إلى عمليات تثبيت غير مسندة، وتقارير استحواذ غير مكتملة، وحسابات غير دقيقة لعائد الاستثمار (ROI) أو تكلفة الاستحواذ على العميل (CAC).

يمكن أن يؤثر فقدان المعلمات أيضاً على تجارب الإعداد. على سبيل المثال، قد يتضمن رابط إحالة الويب معرف دعوة أو رمزاً ترويجياً:

https://example.com/download?inviter_id=USR_1024
https://example.com/download?promo_code=WELCOME50

إذا لم يتمكن التطبيق من استعادة هذه القيم، فقد يحتاج المستخدمون إلى إدخالها يدوياً.

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

آليات المتجر مقابل الآليات المؤجلة: Apple App Store مقابل Google Play

آليات Google Play Store: الاستفادة من Native Install Referrer API

على نظام أندرويد، توفر Google Play Install Referrer API آلية رسمية لاسترداد بيانات الإحالة الوصفية المدعومة للتثبيت.

يمكن أن يتضمن رابط تنزيل Google Play معلمة referrer مشفرة:

https://play.google.com/store/apps/details?id=com.example.app&referrer=utm_source%3Dfacebook%26inviter_id%3DUSR_1024

في رابط URL التوضيحي هذا، تمثل قيمة الإحالة المشفرة:

utm_source=facebook&inviter_id=USR_1024

بعد التثبيت، يمكن لتطبيق أندرويد أصلي استخدام Install Referrer Client لاسترداد معلومات الإحالة المتاحة من Google Play.

يجب التحقق مما إذا كان إصدار معين من OpoInstall Web SDK يقوم بإنشاء أو تعديل أو إدارة مثل هذا الرابط تلقائياً مقابل الوثائق والسلوك المنشور له. لا يثبت وجود واجهة برمجة تطبيقات Google الرسمية التنفيذ الداخلي لحزمة SDK خاصة بطرف ثالث.

حدود Apple App Store: التعامل مع عمليات إعادة التوجيه القياسية للمتجر بدون معلمات استعلام أصلية

لا يقوم Apple App Store بتمرير معلمات استعلام ويب تعسفية مباشرة إلى رد الاتصال الخاص بالتشغيل الأول للتطبيق المثبت حديثاً.

لذلك، قد يتضمن سير عمل الاستعادة المؤجلة التقاط معلومات الحملة المسموح بها قبل إعادة توجيه المتجر وطلب السياق المرتبط عند تشغيل التطبيق الأصلي.

بالنسبة لسير العمل القائم على SDK، يجب على المطورين التمييز بين الوظائف التي يوثقها البائع وطرق المطابقة المستخدمة بواسطة إصدار SDK المنشور.

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

الروابط العالمية (Universal Links) مفيدة لتوجيه الروابط المدعومة إلى تطبيق مثبت بالفعل، لكنها في حد ذاتها لا تضمن بقاء معلمات استعلام الويب التعسفية بعد تثبيت جديد من App Store.

التباعد المعماري عبر المنصات: بناء عقود معلمات موحدة

على الرغم من أن أندرويد وiOS يستخدمان آليات منصة مختلفة، يمكن للتطبيق تطبيع القيم المستعادة إلى مخطط مشترك على مستوى التطبيق.

على سبيل المثال:

{
  "utm_source": "facebook",
  "utm_medium": "paid_social",
  "utm_campaign": "summer_sale",
  "inviter_id": "USR_1024",
  "promo_code": "WELCOME50"
}

يمثل كائن JSON هذا عقد معلمات مطبعاً معرفاً من قبل التطبيق، وليس تنسيق استجابة حرفياً مضموناً من كل إصدار من حزمة OpoInstall SDK.

يجب على التطبيق فصل هذه الحقول إلى فئتين:

  • إسناد التسويق: utm_source، وutm_medium، وutm_campaign.
  • إجراءات الإحالة والترويج: inviter_id وpromo_code.

يمكن تقديم إسناد التسويق إلى خط أنابيب تحليلات معتمد. تتطلب إجراءات الإحالة والترويج تفويضاً مستقلاً من جانب الخادم قبل أن يمنح التطبيق أي ميزة.

دور التخزين المؤقت لحمولة السياق في جسر إعادة توجيه المتجر

تتطلب خدمات الاستعادة المؤجلة عموماً فترة احتفاظ محدودة لسياق التثبيت.

تعتمد نافذة الاحتفاظ المعمول بها، وسلوك الاستعادة، وتوفر المطابقة على الخدمة المنشورة، وتكوين SDK، وظروف الشبكة، وقيود المنصة، ومتطلبات الخصوصية.

يجب على فرق الهندسة قياس نتائج الاستعادة باستخدام مجموعات التثبيت الخاصة بهم بدلاً من تبني ادعاءات دقة غير مدعومة.

هياكل السياق المؤجل المراعية للخصوصية وحدود المنصة

حدود لوحة اللصق للنظام على iOS: لافتات الشفافية وآليات قصد UIPasteControl

استخدمت بعض عمليات الربط المؤجل التاريخية لوحة لصق النظام لحمل السياق المؤقت بين التطبيقات.

ومع ذلك، تقيد iOS وتكشف وصول لوحة اللصق عبر التطبيقات للمستخدمين.

  • iOS 14: قد تؤدي قراءة المحتوى المنسوخ من تطبيق آخر إلى إطلاق إشعار وصول إلى لوحة اللصق.
  • iOS 16 والإصدارات الأحدث: يمكن لبعض عمليات اللصق البرمجية إطلاق مطالبة إذن. تدعم عناصر التحكم مثل UIPasteControl تفاعلات اللصق التي يبدأها المستخدم.

وبالتالي، يجب عدم التعامل مع وصول لوحة اللصق العام كقناة نقل معلمات تثبيت غير مرئية أو غير مقيدة.

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

قيود الحافظة الأمامية لأندرويد: قيود التنقل والمسح التلقائي

يطبق أندرويد أيضاً قيوداً على الوصول إلى الحافظة.

  • Android 10 والإصدارات الأحدث: يتم تقييد الوصول إلى الحافظة للتطبيقات التي ليست في سياق أمامي مؤهل، مع مراعاة الاستثناءات المحددة من قبل المنصة.
  • Android 13 والإصدارات الأحدث: يوفر أندرويد تعليقات واجهة مستخدم متعلقة بالحافظة وسلوك مسح تلقائي لمحتوى الحافظة.

يجب ألا تفترض التطبيقات أنه يمكن استرداد بيانات الحافظة بصمت أو أنها تظل متاحة إلى أجل غير مسمى.

يجب أن يكون الوصول إلى الحافظة اختيارياً ومقتصراً على سير عمل مناسب يواجه المستخدم بدلاً من العمل كقناة إسناد أساسية مفترضة.

الالتزام الصارم بسياسات خصوصية المستخدم واستخدام البيانات الخاصة بشركة Apple: حظر بصمات الجهاز

تحظر Apple بصمات الجهاز المستخدمة لتحديد هوية المستخدمين أو الأجهزة من خلال مجموعات من خصائص الجهاز والبيئة.

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

منح إذن شفافية تتبع التطبيق لا يجعل طرق البصمات المحظورة مقبولة.

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

يجب أن يعطي التكامل المراعي للخصوصية الأولوية لتقليل البيانات، وتفاعلات المستخدم الشفافة، وآليات المنصة المدعومة.

حدود الأمان والثقة: إدارة دورات حياة المعلمات وتفويض الخادم

تتطلب معلمات التثبيت المستعادة معالجة منفصلة للتحليلات وتفويض الخلفية.

يجب التعامل مع جميع المعلمات المستعادة كـ مدخلات غير موثوقة.

يمكن أن تنشأ علامات التسويق ورموز الإحالة من روابط URL قابلة للتحرير أو طلبات عميل غير موثوقة. وجودها وحده لا يثبت أن الحملة شرعية أو أن المستخدم مؤهل للحصول على مكافأة.

يجب على التطبيقات اتباع هذه المبادئ:

  • القيم المدرجة في القائمة المسموحة: اقبل فقط المفاتيح المدعومة صراحةً والقيم المحدودة.
  • فصل الإسناد عن التفويض: قد تصف قيم UTM أصل الزيارات؛ يجب ألا تفوض بشكل مستقل فوائد ترويجية.
  • سجلات معلقة محدودة زمنياً: احتفظ فقط بالمعلومات المطلوبة للمعالجة، مع مراعاة نافذة انتهاء صلاحية محددة من قبل التطبيق.
  • التخزين المحمي: استخدم تخزيناً مناسباً لحساسية الرموز المعلقة، مثل تصميم مدعوم بـ Android Keystore أو خدمات iOS Keychain عند الضرورة.
  • نتائج التسليم المستقلة: بالنسبة للسجلات التي تحتوي على معلمات تسويق ومكافآت، يجب على التطبيق تتبع النتائج المطلوبة بشكل مستقل.
  • التحقق من المكافآت من جانب الخادم: يجب الموافقة على أرصدة الإحالة، والخصومات الترويجية، والاستحقاقات المماثلة من قبل خلفية تطبيق موثقة.
  • تطبيق المكافآت المتطابق (Idempotent): يجب على الخلفية فرض قيود على مستوى المعاملة وعدم التكرار على مستوى العمل بحيث لا يمكن لطلبات العميل المتكررة مضاعفة المكافآت التي تُمنح لمرة واحدة.

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

التنفيذ من البداية إلى النهاية: التقاط معلمات الويب للاستعادة الأصلية

تنقية معلمات استعلام الويب: التمهيد لـ HTML، وتحليل القائمة المسموحة، وإعداد Web JS SDK

يمكن لصفحة هبوط التقاط معلمات استعلام مدعومة صراحةً وتمريرها إلى Web SDK كحمولة محدودة.

يوضح المثال التالي:

  • تحميل SDK من مصدره المكون.
  • الحفاظ على رابط احتياطي عادي لتنزيل HTTPS.
  • قبول مفاتيح المعلمات المدعومة فقط.
  • رفض القيم خارج سياسة حرف وطول ضيقة.
  • ربط إجراء تنزيل SDK بزر التنزيل في الصفحة.

باني SDK، ومفاتيح التكوين، والأساليب هي تمثيلية. تحقق من توقيعاتها مقابل إصدار OpoInstall Web SDK المنشور بالضبط قبل النشر أو التوزيع.

<!-- CODE_BLOCK_01: التمهيد التمثيلي لـ HTML وتكامل Web SDK -->
<!-- File: landing_page.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>تنزيل التطبيق</title>
</head>
<body>
    <!-- استبدل هذا برابط HTTPS حقيقي أو رابط توجيه للمتجر. -->
    <a
        id="download-app-btn"
        href="https://app.example.com/store-download"
    >
        تنزيل التطبيق
    </a>

    <!-- تحقق من رابط CDN الرسمي مقابل إصدار SDK المنشور. -->
    <script src="https://web.cdn.opoinstallcloud.com/openinstall.js"></script>

    <script>
    (function () {
        "use strict";

        function getSanitizedQueryParams() {
            var allowedKeys = new Set([
                "utm_source",
                "utm_medium",
                "utm_campaign",
                "inviter_id",
                "promo_code"
            ]);

            var validPattern = /^[A-Za-z0-9_.-]{1,64}$/;
            var result = Object.create(null);

            try {
                var query = new URLSearchParams(window.location.search);

                query.forEach(function (value, key) {
                    if (
                        allowedKeys.has(key) &&
                        validPattern.test(value) &&
                        !Object.prototype.hasOwnProperty.call(result, key)
                    ) {
                        result[key] = value;
                    }
                });
            } catch (error) {
                console.warn("غير قادر على تحليل معلمات URL.");
                return Object.create(null);
            }

            return result;
        }

        var downloadButton = document.getElementById(
            "download-app-btn"
        );

        // حافظ على href المرساة كخيار احتياطي عادي للمتصفح.
        if (!downloadButton) {
            return;
        }

        if (typeof OpenInstall === "undefined") {
            console.warn(
                "Web SDK غير متاح؛ استخدام رابط التنزيل."
            );
            return;
        }

        var customParams = getSanitizedQueryParams();

        try {
            new OpenInstall({
                appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
                data: customParams,

                onready: function () {
                    var sdkClient = this;

                    downloadButton.addEventListener(
                        "click",
                        function (event) {
                            try {
                                // طريقة SDK تمثيلية.
                                // تحقق من سلوكها وفشل غير متزامن
                                // المعالجة للإصدار المنشور.
                                sdkClient.wakeupOrInstall();
                                event.preventDefault();
                            } catch (error) {
                                console.warn(
                                    "فشل إجراء SDK؛ سيستخدم المتصفح الخيار الاحتياطي."
                                );

                                // لا تمنع إجراء المرساة الافتراضي.
                            }
                        }
                    );
                }
            });
        } catch (error) {
            console.warn(
                "فشل تهيئة Web SDK؛ استخدام الرابط الاحتياطي."
            );
        }
    })();
    </script>
</body>
</html>

تقبل سياسة القيم عمداً الأحرف، والأرقام، والشرطات السفلية، والشرطات، والنقاط فقط، بحد أقصى 64 حرفاً.

يتم استبعاد القيم التي تحتوي على مسافات أو أحرف أخرى في هذا التنفيذ المرجعي. يجب على الفرق ذات متطلبات تسمية UTM الأوسع تحديد واختبار مخطط مناسب خاص بالمجال بدلاً من افتراض صمت أن جميع قيم URL مدعومة.

وبالمثل، تتم مناقشة معرفات نقرات الإعلان مثل gclid وfbclid كمفاهيم إسناد ويب شائعة ولكنها غير مدرجة في هذه القائمة المسموحة للعينة. يتطلب دعمها جمع بيانات، واحتفاظ، ومراجعة خصوصية منفصلة.

يظل الرابط الاحتياطي متاحاً عندما لا يتم تحميل SDK أو لا يمكن تهيئته. نظراً لأن عمليات SDK يمكن أن تفشل بشكل غير متزامن، يجب أن يتبع تكامل الإنتاج بالإضافة إلى ذلك معالجة الأخطاء الموثقة وسلوك إعادة التوجيه الخاص بـ SDK؛ try/catch المتزامن وحده ليس استراتيجية احتياطية غير متزامنة كاملة.

التنفيذ الأصلي لأندرويد: إعداد SDK، والتحقق من المعلمات، وعقود التطبيق المضيف

بعد التثبيت، يمكن لتطبيق أندرويد طلب المعلمات المدعومة من SDK الأصلي وتطبيعها في سجل معرف من قبل التطبيق.

تحافظ العينة أدناه عمداً على فصل المسؤوليات:

  1. تتهيئة OpoInstall SDK في العملية الرئيسية للتطبيق.
  2. يوفر رد الاتصال الأصلي حمولة مرشحة.
  3. يتحقق التطبيق من الحمولة ويقوم بتطبيعها.
  4. يحفظ التطبيق السجل من خلال واجهة تخزين مقدمة من المضيف.
  5. ينسق مكون مقدم من المضيف إسناد التسويق وتفويض المكافآت.
  6. يضع التطبيق علامة على السجل كمكتمل فقط بعد أن يبلغ ذلك المكون عن نتيجة معالجة تم التحقق منها.

هام: واجهات المضيف هي عقود معرفة من قبل التطبيق، وليست واجهات برمجة تطبيقات OpoInstall. يتطلب هذا المثال من التطبيق المضيف تقديم التنفيذات. لا يتضمن تخزيناً احتياطياً غير آمن، أو تسليم تحليلات محاكى، أو استجابة تفويض مكافأة وهمية.

// CODE_BLOCK_02: تكامل أندرويد تمثيلي (Kotlin)
// File: CustomApplication.kt and MainActivity.kt
//
// تحقق من استيرادات OpoInstall وردود الاتصال مقابل ملف AAR المنشور.
// يجب تقديم تنفيذات المضيف قبل معالجة المعلمات.

package com.example.app

import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.opoinstall.api.OpoInstall
import com.opoinstall.api.OpoData
import com.opoinstall.api.OpoError
import com.opoinstall.api.ResultCallBack
import org.json.JSONObject
import java.util.UUID

enum class ParamFetchState {
    NOT_STARTED,
    FETCHING,
    RECEIVED,
    APPLIED,
    REJECTED,
    NO_MATCH,
    RETRYABLE_FAILURE
}

data class PendingAttributionRecord(
    val utmSource: String?,
    val utmMedium: String?,
    val utmCampaign: String?,
    val inviterId: String?,
    val promoCode: String?,
    val idempotencyKey: String,
    val createdAt: Long,
    val expiresAt: Long
) {
    val hasAttributionData: Boolean
        get() = listOf(
            utmSource, utmMedium, utmCampaign
        ).any { !it.isNullOrEmpty() }

    val hasRewardData: Boolean
        get() = !inviterId.isNullOrEmpty() ||
                !promoCode.isNullOrEmpty()

    val hasAnyData: Boolean
        get() = hasAttributionData || hasRewardData

    fun isValid(): Boolean {
        val now = System.currentTimeMillis()
        val pattern = Regex("^[A-Za-z0-9_.-]{1,64}$")

        val values = listOf(
            utmSource, utmMedium, utmCampaign,
            inviterId, promoCode
        )

        val validFields = values.all {
            it == null || pattern.matches(it)
        }

        val validWindow =
            expiresAt > createdAt &&
            expiresAt - createdAt <= 48L * 60 * 60 * 1000 &&
            now >= createdAt &&
            now <= expiresAt

        val validId = try {
            UUID.fromString(idempotencyKey)
            true
        } catch (_: IllegalArgumentException) {
            false
        }

        return hasAnyData &&
               validFields &&
               validWindow &&
               validId
    }
}

// عقد استمرار آمن معرف من قبل المضيف.
// يجب أن يميز التنفيذ بين البيانات المفقودة و
// فشل التخزين والتعامل مع ربط الحساب عند الحاجة.
interface PendingTransactionStore {
    fun save(record: PendingAttributionRecord): Boolean
    fun load(): PendingAttributionRecord?
    fun clear(): Boolean
}

// منسق معرف من قبل المضيف. تنفيذ هذا المنسق مسؤول
// عن الإقرارات المستقلة للقياسات عن بعد/المكافآت،
// وعدم التكرار، ومعالجة إعادة المحاولة، وفحوصات هوية المعاملة.
interface InstallParameterProcessor {
    fun process(
        record: PendingAttributionRecord,
        onComplete: (ProcessingOutcome) -> Unit
    )
}

enum class ProcessingOutcome {
    COMPLETED,
    REJECTED,
    RETRYABLE_FAILURE
}

// هذه هي تبعيات التطبيق، ليست واجهات برمجة تطبيقات OpoInstall.
object HostAppDependencies {
    var transactionStore: PendingTransactionStore? = null
    var parameterProcessor: InstallParameterProcessor? = null

    val isConfigured: Boolean
        get() = transactionStore != null &&
                parameterProcessor != null
}

class CustomApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        if (isMainProcess()) {
            OpoInstall.initialize(this)

            // يجب على التطبيق المضيف تهيئة التبعيات هنا
            // باستخدام آليته الفعلية لـ DI/bootstrap.
            //
            // HostAppDependencies.transactionStore =
            //     hostProvidedProtectedStore
            //
            // HostAppDependencies.parameterProcessor =
            //     hostProvidedInstallParameterProcessor
        }
    }

    private fun isMainProcess(): Boolean {
        val manager = getSystemService(
            Context.ACTIVITY_SERVICE
        ) as ActivityManager

        val currentPid = Process.myPid()

        return manager.runningAppProcesses?.any {
            it.pid == currentPid &&
            it.processName == packageName
        } == true
    }
}

class MainActivity : AppCompatActivity() {

    private val mainHandler = Handler(Looper.getMainLooper())

    private var state = ParamFetchState.NOT_STARTED
    private var activeRecord: PendingAttributionRecord? = null
    private var processingInFlight = false

    private val transactionTtlMs =
        24L * 60 * 60 * 1000

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startParameterFlow()
    }

    private fun startParameterFlow() {
        val store = HostAppDependencies.transactionStore
        val processor = HostAppDependencies.parameterProcessor

        if (store == null || processor == null) {
            Log.w(
                "OpoInstall",
                "تبعية المضيف غير متاحة؛ المعالجة معلقة"
            );
            state = ParamFetchState.RETRYABLE_FAILURE
            return
        }

        // استرداد سجل تمثيلي.
        // يجب أن يبلغ التخزين في الإنتاج عن فشل القراءة بشكل منفصل
        // عن الغياب المؤكد لسجل معلق.
        val existingRecord = try {
            store.load()
        } catch (error: Exception) {
            state = ParamFetchState.RETRYABLE_FAILURE
            return
        }

        if (existingRecord != null) {
            if (existingRecord.isValid()) {
                activeRecord = existingRecord
                state = ParamFetchState.RECEIVED
                processRecord(existingRecord)
            } else {
                // السجلات غير الصالحة أو منتهية الصلاحية لا تتم معالجتها.
                if (!store.clear()) {
                    state = ParamFetchState.RETRYABLE_FAILURE
                    return
                }

                requestInstallParameters()
            }
        } else {
            requestInstallParameters()
        }
    }

    private fun requestInstallParameters() {
        if (!HostAppDependencies.isConfigured) {
            state = ParamFetchState.RETRYABLE_FAILURE
            return
        }

        if (state == ParamFetchState.FETCHING ||
            state == ParamFetchState.APPLIED ||
            state == ParamFetchState.NO_MATCH ||
            processingInFlight
        ) {
            return
        }

        state = ParamFetchState.FETCHING

        // رد اتصال SDK تمثيلي.
        // تحقق من التوقيع مقابل ملف AAR المنشور.
        OpoInstall.getInstance().getInstallParam(
            object : ResultCallBack<OpoData> {

                override fun onResult(opoData: OpoData?) {
                    mainHandler.post {
                        if (state != ParamFetchState.FETCHING) {
                            return@post
                        }

                        val rawData: Any? = opoData?.data

                        val payload = when (rawData) {
                            is String -> rawData
                            is JSONObject -> rawData.toString()
                            is Map<*, *> -> try {
                                JSONObject(rawData).toString()
                            } catch (_: Exception) {
                                null
                            }
                            else -> null
                        }

                        val record = payload?.let {
                            parseAndSanitizeRawPayload(it)
                        }

                        if (record == null) {
                            // لم يتم استرداد بيانات مؤهلة.
                            // هذا لا يثبت تثبيتاً عضوياً.
                            state = ParamFetchState.NO_MATCH
                            return@post
                        }

                        val store =
                            HostAppDependencies.transactionStore

                        if (store == null ||
                            !HostAppDependencies.isConfigured
                        ) {
                            state = ParamFetchState.RETRYABLE_FAILURE
                            return@post
                        }

                        activeRecord = record

                        // لا تطالب باسترداد دائم إذا فشل الحفظ.
                        val saved = try {
                            store.save(record)
                        } catch (_: Exception) {
                            false
                        }

                        if (!saved) {
                            Log.w(
                                "OpoInstall",
                                "لم يتم استمرار السجل المعلق"
                            );
                            state = ParamFetchState.RETRYABLE_FAILURE
                            return@post
                        }

                        state = ParamFetchState.RECEIVED
                        processRecord(record)
                    }
                }

                override fun onError(error: OpoError?) {
                    mainHandler.post {
                        if (state == ParamFetchState.FETCHING) {
                            Log.w(
                                "OpoInstall",
                                "فشل استرداد معلمات التثبيت"
                            );
                            state = ParamFetchState.RETRYABLE_FAILURE
                        }
                    }
                }
            }
        )
    }

    private fun parseAndSanitizeRawPayload(
        rawJson: String
    ): PendingAttributionRecord? {
        return try {
            if (rawJson.length > 4096) return null

            val json = JSONObject(rawJson)
            val pattern = Regex("^[A-Za-z0-9_.-]{1,64}$")

            fun field(key: String): String? =
                (json.opt(key) as? String)?.takeIf {
                    pattern.matches(it)
                }

            val now = System.currentTimeMillis()

            val record = PendingAttributionRecord(
                utmSource = field("utm_source"),
                utmMedium = field("utm_medium"),
                utmCampaign = field("utm_campaign"),
                inviterId = field("inviter_id"),
                promoCode = field("promo_code"),
                idempotencyKey = UUID.randomUUID().toString(),
                createdAt = now,
                expiresAt = now + transactionTtlMs
            )

            record.takeIf { it.isValid() }
        } catch (_: Exception) {
            null
        }
    }

    private fun processRecord(
        record: PendingAttributionRecord
    ) {
        val processor = HostAppDependencies.parameterProcessor
        val store = HostAppDependencies.transactionStore

        if (processor == null || store == null ||
            !record.isValid()
        ) {
            state = ParamFetchState.RETRYABLE_FAILURE
            return
        }

        if (processingInFlight) return

        processingInFlight = true

        // يجب ألا يبلغ معالج المضيف عن COMPLETED حتى
        // يتم تأكيد جميع النتائج المحددة لكل قناة.
        processor.process(record) { outcome ->
            mainHandler.post {
                // تجاهل المعاملات المستبدلة أو القديمة.
                if (activeRecord?.idempotencyKey !=
                    record.idempotencyKey
                ) {
                    return@post
                }

                processingInFlight = false

                when (outcome) {
                    ProcessingOutcome.COMPLETED -> {
                        finalizeRecord(
                            record,
                            ParamFetchState.APPLIED
                        )
                    }

                    ProcessingOutcome.REJECTED -> {
                        finalizeRecord(
                            record,
                            ParamFetchState.REJECTED
                        )
                    }

                    ProcessingOutcome.RETRYABLE_FAILURE -> {
                        state =
                            ParamFetchState.RETRYABLE_FAILURE
                    }
                }
            }
        }
    }

    private fun finalizeRecord(
        record: PendingAttributionRecord,
        terminalState: ParamFetchState
    ) {
        val store = HostAppDependencies.transactionStore
            ?: run {
                state = ParamFetchState.RETRYABLE_FAILURE
                return
            }

        if (activeRecord?.idempotencyKey !=
            record.idempotencyKey
        ) {
            return
        }

        // يترك هذا المثال عمداً تدوين نهائي دائم
        // والتنظيف الذري لمتجر المضيف.
        // يجب أن يبلغ clear() عما إذا نجح التنظيف.
        val cleared = try {
            store.clear()
        } catch (_: Exception) {
            false
        }

        if (!cleared) {
            state = ParamFetchState.RETRYABLE_FAILURE
            return
        }

        activeRecord = null
        state = terminalState

        Log.i(
            "OpoInstall",
            "تم حل معالجة معلمات التثبيت"
        )
    }
}

لا تتضمن عينة أندرويد عمداً تنفيذاً افتراضياً لـ PendingTransactionStore أو InstallParameterProcessor.

يجب أن يقدم التطبيق المضيف هذه قبل استدعاء سير العمل.

على وجه الخصوص، يجب ألا يعيد المعالج COMPLETED لمجرد تشغيل بيان تسجيل أو بدء طلب غير متزامن. يجب أن تتوافق النتيجة المكتملة مع متطلبات المعالجة للحمولة الفعلية:

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

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

يوضح النشاط أعلاه حد التكامل؛ إنه ليس بديلاً عن تنفيذات التطبيق المحددة تلك.

التنفيذ الأصلي لـ iOS: استعادة SDK وعقود تكامل التطبيق

يتبع تكامل iOS نفس الحدود.

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

الكود تمثيلي ويجب تكييفه مع اسم الوحدة، وبروتوكول المندوب، وتوقيعات رد الاتصال لإصدار OpoInstall iOS SDK المثبت.

// CODE_BLOCK_03: تكامل iOS تمثيلي (Swift)
// File: AppDelegate.swift
//
// تحقق من الوحدة وطرق SDK مقابل إصدار إطار عمل
// OpoInstall المنشور.

import UIKit
import libOpoInstallSDK

enum ParamFetchState: String {
    case notStarted = "NOT_STARTED"
    case fetching = "FETCHING"
    case received = "RECEIVED"
    case applied = "APPLIED"
    case rejected = "REJECTED"
    case noMatch = "NO_MATCH"
    case retryableFailure = "RETRYABLE_FAILURE"
}

struct PendingAttributionRecord: Codable {
    let utmSource: String?
    let utmMedium: String?
    let utmCampaign: String?
    let inviterId: String?
    let promoCode: String?
    let idempotencyKey: String
    let createdAt: TimeInterval
    let expiresAt: TimeInterval

    var hasAttributionData: Bool {
        return [
            utmSource, utmMedium, utmCampaign
        ].contains { !$0.isNilOrEmpty }
    }

    var hasRewardData: Bool {
        return !inviterId.isNilOrEmpty ||
               !promoCode.isNilOrEmpty
    }

    var hasAnyData: Bool {
        return hasAttributionData || hasRewardData
    }

    var isValid: Bool {
        let now = Date().timeIntervalSince1970
        let window = expiresAt - createdAt

        let pattern = "^[A-Za-z0-9_.-]{1,64}$"

        let fields = [
            utmSource, utmMedium, utmCampaign,
            inviterId, promoCode
        ]

        let validFields = fields.allSatisfy { field in
            guard let field = field else { return true }
            return field.range(
                of: pattern,
                options: .regularExpression
            ) != nil
        }

        return hasAnyData &&
               validFields &&
               UUID(uuidString: idempotencyKey) != nil &&
               window > 0 &&
               window <= 48 * 60 * 60 &&
               now >= createdAt &&
               now <= expiresAt
    }
}

private extension Optional where Wrapped == String {
    var isNilOrEmpty: Bool {
        return self?.isEmpty ?? true
    }
}

// عقد استمرار محمي مقدم من المضيف.
protocol PendingTransactionStore {
    func save(_ record: PendingAttributionRecord) -> Bool
    func load() throws -> PendingAttributionRecord?
    func clear() -> Bool
}

enum ProcessingOutcome {
    case completed
    case rejected
    case retryableFailure
}

// منسق مقدم من المضيف لتحليلات ومعالجة المكافآت.
protocol InstallParameterProcessor {
    func process(
        record: PendingAttributionRecord,
        completion: @escaping (ProcessingOutcome) -> Void
    )
}

// هذه عقود تطبيق، ليست واجهات OpoInstall.
struct HostAppDependencies {
    static var transactionStore: PendingTransactionStore?
    static var parameterProcessor: InstallParameterProcessor?

    static var isConfigured: Bool {
        return transactionStore != nil &&
               parameterProcessor != nil
    }
}

@UIApplicationMain
class AppDelegate:
    UIResponder,
    UIApplicationDelegate,
    OpoInstallDelegate {

    var window: UIWindow?

    private var state: ParamFetchState = .notStarted
    private var activeRecord: PendingAttributionRecord?
    private var processingInFlight = false

    private let transactionTTL: TimeInterval =
        24 * 60 * 60

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

        // تهيئة SDK تمثيلية.
        OpoInstallSDK.initWith(self)

        // يجب على التطبيق المضيف تسجيل تبعياته الفعلية
        // قبل بدء معالجة المعلمات.
        //
        // HostAppDependencies.transactionStore =
        //     hostProvidedProtectedStore
        //
        // HostAppDependencies.parameterProcessor =
        //     hostProvidedInstallParameterProcessor

        startParameterFlow()
        return true
    }

    private func startParameterFlow() {
        guard HostAppDependencies.isConfigured,
              let store = HostAppDependencies.transactionStore
        else {
            print(
                "OpoInstall: تبعيات المضيف المطلوبة غير متاحة"
            );
            state = .retryableFailure
            return
        }

        do {
            if let saved = try store.load() {
                if saved.isValid {
                    activeRecord = saved
                    state = .received
                    processRecord(saved)
                    return
                }

                guard store.clear() else {
                    state = .retryableFailure
                    return
                }
            }
        } catch {
            // خطأ القراءة لا يعادل متجراً فارغاً.
            state = .retryableFailure
            return
        }

        requestInstallParameters()
    }

    private func requestInstallParameters() {
        guard HostAppDependencies.isConfigured,
              state != .fetching,
              state != .applied,
              state != .noMatch,
              !processingInFlight
        else {
            return
        }

        guard let manager = OpoInstallSDK.defaultManager()
        else {
            state = .retryableFailure
            return
        }

        state = .fetching

        // توقيع رد اتصال تمثيلي.
        manager.getInstallParmsCompleted({
            [weak self] (appData: OpoinstallData?) in

            DispatchQueue.main.async {
                guard let self = self,
                      self.state == .fetching
                else {
                    return
                }

                let rawData: Any? = appData?.data

                guard let record =
                    self.parseAndSanitizeRawPayload(rawData)
                else {
                    // لم يتم استرداد معلمات مخصصة مؤهلة.
                    // هذا ليس دليلاً قاطعاً على حركة مرور عضوية.
                    self.state = .noMatch
                    return
                }

                guard HostAppDependencies.isConfigured,
                      let store =
                          HostAppDependencies.transactionStore
                else {
                    self.state = .retryableFailure
                    return
                }

                self.activeRecord = record

                guard store.save(record) else {
                    // لا تدعي وجود سجل معلق دائم
                    // عندما فشل الاستمرار.
                    self.state = .retryableFailure
                    return
                }

                self.state = .received
                self.processRecord(record)
            }
        })
    }

    private func parseAndSanitizeRawPayload(
        _ rawData: Any?
    ) -> PendingAttributionRecord? {

        guard let rawData = rawData else {
            return nil
        }

        var dictionary: [String: Any]?

        if let map = rawData as? [String: Any] {
            dictionary = map
        } else if let jsonString = rawData as? String,
                  jsonString.count <= 4096,
                  let data = jsonString.data(using: .utf8),
                  let parsed = try? JSONSerialization.jsonObject(
                      with: data
                  ) as? [String: Any] {
            dictionary = parsed
        }

        guard let payload = dictionary else {
            return nil
        }

        let pattern = "^[A-Za-z0-9_.-]{1,64}$"

        func field(_ key: String) -> String? {
            guard let value = payload[key] as? String,
                  value.range(
                    of: pattern,
                    options: .regularExpression
                  ) != nil
            else {
                return nil
            }

            return value
        }

        let now = Date().timeIntervalSince1970

        let record = PendingAttributionRecord(
            utmSource: field("utm_source"),
            utmMedium: field("utm_medium"),
            utmCampaign: field("utm_campaign"),
            inviterId: field("inviter_id"),
            promoCode: field("promo_code"),
            idempotencyKey: UUID().uuidString,
            createdAt: now,
            expiresAt: now + transactionTTL
        )

        return record.isValid ? record : nil
    }

    private func processRecord(
        _ record: PendingAttributionRecord
    ) {
        guard record.isValid,
              let processor =
                  HostAppDependencies.parameterProcessor
        else {
            state = .retryableFailure
            return
        }

        guard !processingInFlight else {
            return
        }

        processingInFlight = true

        // يجب على معالج المضيف تأكيد
        // نتائج الإسناد والمكافأة المطلوبة بشكل مستقل.
        processor.process(record: record) {
            [weak self] outcome in

            DispatchQueue.main.async {
                guard let self = self,
                      self.activeRecord?.idempotencyKey ==
                          record.idempotencyKey
                else {
                    return
                }

                self.processingInFlight = false

                switch outcome {
                case .completed:
                    self.finalizeRecord(
                        record,
                        terminalState: .applied
                    )

                case .rejected:
                    self.finalizeRecord(
                        record,
                        terminalState: .rejected
                    )

                case .retryableFailure:
                    self.state = .retryableFailure
                }
            }
        }
    }

    private func finalizeRecord(
        _ record: PendingAttributionRecord,
        terminalState: ParamFetchState
    ) {
        guard activeRecord?.idempotencyKey ==
                  record.idempotencyKey,
              let store =
                  HostAppDependencies.transactionStore
        else {
            state = .retryableFailure
            return
        }

        // يجب على متجر المضيف تنسيق الإكمال الدائم
        // والتنظيف لتلبية احتياجات موثوقية التطبيق.
        guard store.clear() else {
            state = .retryableFailure
            return
        }

        activeRecord = nil
        state = terminalState

        print(
            "OpoInstall: تم حل معالجة معلمات التثبيت"
        )
    }
}

لا يقدم مثال iOS متجراً للمعاملات المعلقة مدعوماً بـ UserDefaults أو عميل قياس عن بعد يبلغ عن النجاح تلقائياً.

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

حيثما يعالج التطبيق قيماً تحمل مكافآت، يجب ألا يعيد المعالج .completed قبل انتهاء التفويض من جانب الخادم.

حيثما تتضمن الحمولة بيانات تسويقية فقط، يجب أن يعكس الإكمال قبول التطبيق للتحليلات أو متطلبات قائمة الانتظار الموثوقة.

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

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

حدود تكامل الإنتاج وقائمة التحقق من الصحة

قبل توزيع أي من مثالي الجوال، يجب على فرق الهندسة التحقق من الشروط التالية.

حد التكامل التحقق المطلوب للإنتاج
Web SDK تأكيد مصدر CDN، وتكوين الباني، وطريقة التنزيل، وسلوك الفشل غير المتزامن للإصدار المنشور.
Native SDK تأكيد أسماء الحزمة/الوحدة، ومتطلبات التهيئة، وتوقيعات رد الاتصال، وأنواع الحمولة مقابل ملف AAR للأندرويد أو إطار عمل iOS بالضبط.
مخطط المدخلات تأكيد أسماء المعلمات المدعومة، وأطوال القيم، وقواعد الأحرف، والتعامل مع المفاتيح المشوهة أو المكررة.
التخزين المحمي استخدم تنفيذ تخزين مقدم من التطبيق مع حماية مناسبة، وانتهاء الصلاحية، وإبلاغ عن الأخطاء.
القياس عن بعد التسويقي تأكيد أن الإكمال الناجح يعني قبول حدث تحليلات بشكل موثوق أو وضعه في قائمة انتظار دائمة، بدلاً من مجرد تسجيله.
تفويض المكافأة استخدم التحقق من خلفية التطبيق الموثقة؛ لا تمنح مكافأة بناءً على معلمات مقدمة من العميل فقط.
الحمولات المختلطة الحفاظ على عمل الإسناد والمكافأة المعلق بشكل مستقل حتى يتم حل كل نتيجة مطلوبة.
عدم التكرار (Idempotency) الحفاظ على هوية المعاملة مستقرة عبر عمليات إعادة المحاولة وفرض فرادة المكافأة على الخادم.
إصدارات التوزيع التحقق من إصدارات أندرويد المصغرة وإصدارات iOS مع أدوات SDK الفعلية.
مراجعة الخصوصية التحقق من سلوك جمع البيانات والمطابقة الفعلي لـ SDK مقابل متطلبات المنصة والتنظيمية.

هذه هي معايير قبول التكامل، وليست ادعاءات بأن الكود المرجعي أو إصدار معين من SDK قد اجتاز بالفعل اختبارات الإنتاج.

مصفوفة مقارنة استراتيجية تمرير المعلمات

تحليل منهجي لاستراتيجيات استعادة المعلمات

تستخدم أنظمة الاستحواذ على الجوال أساليب مختلفة للحفاظ على سياق الإحالة والإسناد بين زيارات الويب والتطبيقات الأصلية.

يعتمد النهج المناسب على المنصة المستهدفة، وأنواع المعلمات المطلوبة، والاحتكاك المقبول للمستخدم، وحساسية البيانات، وآليات المنصة المتاحة.

استراتيجية المعلمة تغطية الاستعادة احتكاك المستخدم اعتبارات الخصوصية / السياسة التفويض من الخلفية وعدم التكرار مطلوب
إدخال الرمز الترويجي اليدوي يعتمد على مشاركة المستخدم وتوفر الرمز. عالي؛ يقوم المستخدمون بإدخال الرموز يدوياً. لا يتطلب مطابقة الجهاز لإدخال الرمز، ولكن قواعد حماية البيانات العادية لا تزال تطبق. نعم، لاسترداد المكافآت.
Google Play Install Referrer عمليات تثبيت Google Play على تدفقات أندرويد المدعومة. حد أدنى عند التنفيذ بنجاح. نقل مدعوم رسمياً من المتجر؛ لا تزال المعالجة اللاحقة واستخدام البيانات تتطلب مراجعة. نعم، عندما يمكن لقيم الإحالة إطلاق مكافآت.
استعادة معلمات OpoInstall يعتمد على سلوك SDK المنشور، ومسار الدخول، والمنصة، وآليات المطابقة المسموح بها. عادة ما يكون حده الأدنى عند نجاح الاستعادة. يتطلب تقييم ممارسات بيانات SDK، والأذونات، وقواعد المنصة. نعم، للمعلمات الحاملة للمكافآت.

تقييم الدقة، والامتثال للخصوصية، واحتكاك المستخدم، وحدود الأمان

من السهل تقنياً فهم إدخال الرمز الترويجي اليدوي ولكنه قد يسبب احتكاكاً للمستخدم.

يوفر Google Play Install Referrer قناة إحالة أندرويد أصلية ومدعومة من المتجر.

يمكن لـ SDK استعادة المعلمات من طرف ثالث توفير طبقة تكامل على مستوى التطبيق للسياق المؤجل المؤهل. ومع ذلك، يجب تقييم تغطية التسليم الفعلية، وآليتها التقنية، وخصائص الخصوصية الخاصة بها باستخدام الإصدار المنشور.

بغض النظر عن طريقة الاستعادة، يجب على التطبيق التمييز بين تلقي معلمة وتفويض إجراء تجاري.

استكشاف أخطاء استعادة المعلمات، وإدارة الحالة، وأخطاء تكامل SDK وإصلاحها

إدارة انتقالات حالة جانب العميل: التعامل مع عدم التطابق، وإعادة المحاولة، والنتائج النهائية

تحتاج حمولات الإسناد والمكافأة المختلطة إلى حالات إكمال مستقلة قبل التنظيف.

يمكن وصف سير عمل استعادة المعلمة باستخدام سبع حالات منطقية:

الحالة المعنى
NOT_STARTED لم تبدأ أي محاولة استرداد.
FETCHING التطبيق يطلب معلمات التثبيت.
RECEIVED تم تلقي حمولة مرشحة وقبولها للمعالجة.
APPLIED يبلغ معالج المضيف أن جميع النتائج الناجحة المطلوبة لهذا السجل قد اكتملت.
REJECTED يبلغ معالج المضيف عن رفض نهائي محدد.
NO_MATCH لم تنتج محاولة الاسترداد معلمات مخصصة مؤهلة.
RETRYABLE_FAILURE لا يمكن لسير العمل المتابعة حالياً وقد يتم إعادة المحاولة بموجب سياسة محكومة.

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

على وجه الخصوص، يمكن أن يكتمل سجل التسويق فقط بدون تفويض المكافأة، بينما يجب أن يحصل السجل الحامل للمكافأة على استجابة الخادم المناسبة.

لا ينبغي تفسير نتيجة NO_MATCH كدليل قاطع على أن التثبيت كان عضوياً.

حل مهلات طلب المعلمات وتأخيرات الشبكة برشاقة

يمكن أن تتأثر استعادة المعلمة بالاتصال، وتوقيت رد الاتصال، وترتيب بدء تشغيل التطبيق، أو خدمات الخلفية غير المتاحة.

يجب على التطبيقات:

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

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

إدارة تثبيتات الكتابة فوق التطبيق ومخازن المعلمات القديمة

تستهدف استعادة المعلمة عادةً التشغيل الأول بعد تثبيت مؤهل، ولكن ترقيات التطبيق وعمليات التشغيل المتكررة يمكن أن تكشف عن حالة محلية مخزنة مسبقاً.

يجب على فرق الهندسة تحديد متى تكون محاولة الاستعادة مؤهلة، وكيف يتم تحديد السجلات المستهلكة بالفعل، وكيف تنتهي صلاحية البيانات المعلقة.

بالنسبة للمعلمات الحاملة للمكافآت، يعد عدم التكرار من جانب الخادم أمراً ضرورياً. علامة جانب العميل وحدها ليست حماية كافية ضد المكافآت المكررة.

عندما يحتفظ التطبيق بمعاملة معلقة محلياً، يجب عليه إزالة السجل فقط بعد حل نتائج المعالجة المطلوبة بشكل دائم.

منع مشكلات إعادة التهيئة متعددة العمليات في أندرويد

قد تنفذ تطبيقات أندرويد ذات العمليات المتعددة Application.onCreate() أكثر من مرة.

إذا كان المقصود من SDK الأصلية التهيئة فقط في عملية التطبيق الأساسية، يجب على المطورين التحقق من العملية الحالية قبل تهيئتها.

توضح عينة أندرويد استراتيجية واحدة لمقارنة اسم العملية. يجب على تطبيقات الإنتاج التحقق من النهج مقابل إصدارات أندرويد المدعومة ومعمارية العملية.

تصحيح أخطاء عدم تطابق توقيع رد اتصال العميل وقواعد تعتيم مقلص الكود

يمكن أن تختلف أسماء حزم SDK التابعة لطرف ثالث، وواجهات رد الاتصال، وإعلانات الفئات عبر الإصدارات.

عند فشل التكامل، يجب على المطورين التحقق من SDK المنشور بدلاً من افتراض أن واجهات العينة تظل دون تغيير.

بالنسبة لإصدارات توزيع أندرويد:

  • فضل قواعد Consumer R8 / ProGuard المقدمة مع أداة SDK الدقيقة.
  • تجنب قواعد -keep أو -dontwarn الواسعة على مستوى الحزمة بدون أدلة.
  • افحص متطلبات الانعكاس الفعلية قبل إضافة قواعد تكميلية ضيقة.
  • اختبر رد اتصال معلمة التثبيت في تكوين توزيع مصغر.

الكود في هذا الدليل هو تكامل تمثيلي ولا ينبغي وصفه بأنه تم التحقق من توزيعه بدون أدلة بناء وتشغيل مقابلة.

الأسئلة الشائعة (FAQ)

كيف تستمر معلمات التتبع عند تثبيت مستخدم لتطبيق عبر متجر التطبيقات؟
تستمر معلمات التتبع من خلال خطوط أنابيب استعادة المعلمات المؤجلة. عندما يتفاعل المستخدم مع صفحة هبوط على الويب، تقوم OpoInstall Web JS SDK بتعبئة معلمات الاستعلام وإرسالها إلى خلفية الإسناد. عندما يقوم المستخدم بتشغيل التطبيق المثبت حديثاً لأول مرة، تقوم حزمة OpoInstall SDK الأصلية بالاستعلام عن واجهات برمجة تطبيقات الخلفية أو Play Store، ومطابقة الجلسة وتسليم معلمات استعلام الويب المخصصة إلى التطبيق.
ما هي قيود الخصوصية التي تنطبق على حمولات الحافظة على iOS وأندرويد؟
على iOS 14 والإصدارات الأحدث، قد يؤدي الوصول إلى لوحة اللصق العامة عبر التطبيقات إلى إطلاق لافتة إشعار النظام، وتقدم iOS 16 مطالبات إذن اللصق للقراءات البرمجية ما لم يتم البدء بها عبر عناصر تحكم مثل `UIPasteControl`. على Android 10 والإصدارات الأحدث، يقتصر الوصول إلى الحافظة على التطبيقات الأمامية، ويوفر Android 13 تأكيداً لواجهة المستخدم في النظام عند نسخ المحتوى. وبالتالي، يجب التعامل مع حمولات الحافظة كإشارة نقل اختيارية وحساسة للخصوصية بدلاً من كونها قناة أساسية غير مقيدة.
هل يمكن لمعلمات التتبع حمل حمولات JSON متداخلة للتوجيه الديناميكي داخل التطبيق؟
بينما يمكن لبروتوكولات الشبكة الأساسية نقل حمولات JSON كسلاسل نصية، تفرض هياكل أمان الإنتاج—بما في ذلك التنفيذ المرجعي في هذا الدليل—مخططات عددية صارمة مدرجة في قائمة السماح (مثل سلاسل أبجدية رقمية ورموز حملة يصل طولها إلى 64 حرفاً) للتخفيف من حقن النصوص البرمجية ومخاطر حدود الذاكرة. إذا كان التوجيه المعقد داخل التطبيق مطلوباً، يجب على التطبيقات تمرير معرف مسار مبهم وحل مسارات التنقل الغنية بأمان من تكوين الخلفية الموثوقة بدلاً من تحليل هياكل JSON المتداخلة غير المحققة مباشرة على العميل.

الملخص وإطار القرار

يتطلب ربط نقرات الويب بعمليات تثبيت تطبيقات الجوال نهج استعادة معلمات يحترم الحدود التقنية بين المتصفحات، ومتاجر التطبيقات، والتطبيقات الأصلية.

بالنسبة لنظام أندرويد، توفر Google Play Install Referrer API آلية إحالة رسمية لتدفقات التثبيت المدعومة. بالنسبة لنظام iOS، لا يتم تسليم معلمات الويب التعسفية تلقائياً عبر متجر التطبيقات، لذا تتطلب الاستعادة المؤجلة نهجاً مراعياً للخصوصية ومتحققاً منه بشكل مستقل.

يمكن لتكامل OpoInstall العملي التقاط معلمات صفحة الهبوط المدرجة في القائمة المسموحة وطلب السياق المؤهل من خلال SDK أصلية. يجب على التطبيق بعد ذلك تطبيع البيانات المستعادة والتحقق منها قبل استخدامها لإسناد التسويق، أو التنقل، أو سير عمل الترويج.

الفرق الهندسي الرئيسي هو بين استعادة المعلمات والمعالجة من جانب التطبيق. استعادة معلمة لا تضمن تسليم التحليلات، أو أهلية المكافأة، أو التفويض الناجح.

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

لاستكشاف متطلبات تكامل البائع، راجع وثائق OpoInstall SDK أو صفحة تسجيل مطوري OpoInstall.

المواد ذات الصلة

المفاهيم: تثبيت معلمات التطبيق، Google Play Install Referrer، استعادة المعلمات المؤجلة، قياس الإسناد عن بعد، حد تفويض الخادم.

التقنيات: Web JS SDK، Native Mobile SDK، معلمات استعلام URL، Android Keystore، خدمات iOS Keychain.

المعايير: بناء جملة URI IETF RFC 3986، معيار التحقق من أمان تطبيقات الجوال OWASP، واجهة برمجة تطبيقات الحافظة وأحداث W3C.

واجهات برمجة التطبيقات: واجهة برمجة تطبيقات معلمات التثبيت الأصلية لـ OpoInstall، Google Play Install Referrer API، واجهة برمجة تطبيقات Apple UIPasteboard، واجهات برمجة تطبيقات الحافظة لأندرويد.

التوثيق الرسمي والمراجع:

Share this article