جاوا (Java)

قبل از شروع، توصیه می‌شود برای آشنایی با مفاهیم اصلی، این نوشته را بخوانید تا پیاده‌سازی برای شما راحت‌تر شود.
در صورتی که قبلا پباده‌سازی پرداخت درون‌برنامه‌ای مایکت را با روش قدیمی انجام داده‌اید، پیشنهاد می‌کنیم از روش جدید پیاده‌سازی (در همین صفحه) استفاده نمایید. اما اگر بنا به دلایلی انتخاب شما پیاده‌سازی با روش قبلی است، این لینک را ببینید. توجه کنید که کد روش پیاده‌سازی قدیمی دیگر توسط مایکت پشتیبانی نمی‌شود.

مقدمه

خرید درون‌برنامه‌ای مایکت API ساده‌ای برای شما فراهم کرده است تا به راحتی بتوانید امور مربوط به «خرید» را مدیریت کنید. این نوشته تمام نیازمندی‌های شما برای پیاده‌سازی خرید درون برنامه‌ای در کد اندروید را شرح می‌دهد، هر چند ممکن است برای آشنایی بیشتر با مفاهیم و مراحل فرآیند خرید درون برنامه‌ای، نیاز باشد سایر مستندات این پایگاه دانش را مطالعه کنید.

آشنایی با مدل‌های پیاده‌سازی محصولات درون‌برنامه‌ای

سه مدل کلی برای کسب درآمد از طریق خرید درون‌برنامه‌ای وجود دارد: محصولات مصرف‌شدنی، مصرف‌نشدنی و اشتراکی.

محصولات مصرف‌شدنی، محصولاتی هستند که کاربر پس از استفاده از آن‌ها نسبت به خرید مجدد این محصولات اقدام می‌کند. مثال اینگونه محصولات، سکه، GEM و… است.

محصولات مصرف‌نشدنی، محصولاتی هستند که کاربران یک بار برای همیشه مبلغی بابت تهیه آن‌ها پرداخت می‌کنند. مانند باز کردن مراحلی از بازی، حذف تبلیغات و تهیه اکانت پریمیوم.

محصولات اشتراکی، برای مدت زمان محدودی در اختیار کاربران قرار می‌گیرند. مثلا اشتراک یک ماهه یک برنامه موسیقی یا اشتراک یک ساله یک کتابخوان دیجیتالی. در این مدل، مدت زمان مجاز برای استفاده از برنامه، توسط استور کنترل می‌شود و نیازی به هیچ پیاده‌سازی جهت کنترل زمان نیست. شایان ذکر است در حال حاضر مدل اشتراکی در مایکت پشتیبانی نمی‌شود.

بارگذاری برنامه در پنل توسعه‌دهندگان

برای شروع باید یک حساب توسعه‌دهنده بسازید و برنامه خود را در پنل توسعه‌دهندگان تعریف کنید. سپس،‌ کافی است کلید رمز عمومی برنامه خود را دریافت و محصولات خود را تعریف نمایید. توجه کنید که برای اضافه کردن برنامه به پنل توسعه‌دهندگان مایکت نیازی نیست حتما apk برنامه خود را بارگذاری کنید. تنها کافی است، در قسمت اضافه کردن برنامه یک شناسه (Packagename) برای برنامه خود رزرو کنید.

دریافت کتابخانه پرداخت

در این پروژه این امکان وجود دارد که با استفاده از Build Variantهای Gradle بتوانید به سرویس پرداخت چند Store (کافه‌بازار، گوگل‌پلی و…) متصل شوید. این امکان به شما کمک می‌کند تا فرایند نگه‌داری سورس-کد و انتشار پروژه را برای چند فروشگاه اندرویدی، راحت‌تر کنید.

برای مشاهده و دریافت برنامه نمونه و سورس-کد آن می‌توانید به اینجا مراجعه کنید.

برای پیاده‌سازی پرداخت درون‌برنامه‌ای مایکت با این روش، کافی است مراحل زیر را دنبال کنید:

۱. در فایل build.gradle موجود در پوشه‌ اصلی پروژه خود Jitpack Repository را اضافه کنید:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

۲. در فایل build.gradle پروژه خود کتابخانه پرداخت مایکت را اضافه کنید:

dependencies {
    ...
    implementation 'com.github.myketstore:myket-billing-client:1.6'
}

۳. این تکه کد را در قسمت android موجود در فایل build.gradle پروژه خود اضافه کنید:

android {
    defaultConfig {
        def marketApplicationId = "ir.mservices.market"
        def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
        manifestPlaceholders = [marketApplicationId: "${marketApplicationId}",
                marketBindAddress  : "${marketBindAddress}",
                marketPermission   : "${marketApplicationId}.BILLING"]
        buildConfigField "String", "IAB_PUBLIC_KEY", "\"{MYKET_PUBLIC_KEY}\""
    }
    ...
}

توجه کنید، در صورتی که از build.gradle.kts استفاده می‌کنید، از تکه کد زیر استفاده کنید:

android {
    defaultConfig {
    val marketApplicationId = "ir.mservices.market"
    val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
    manifestPlaceholders.apply {
        this["marketApplicationId"] = marketApplicationId
        this["marketBindAddress"] = marketBindAddress
        this["marketPermission"] = "${marketApplicationId}.BILLING"
    }
    buildConfigField(
        "String",
        "IAB_PUBLIC_KEY",
        "YOUR_KEY"
    )
    }
    ...
}

در صورتی که می‌خواهید به غیر از سرویس پرداخت درون‌برنامه‌ای مایکت به سرویس پرداخت استو‌رهای دیگر متصل شوید، کافی است به جای defaultConfig از productFlavors استفاده کنید:

android {
    flavorDimensions "store"
 
    productFlavors {
        myket {
            dimension "store"
            def marketApplicationId = "ir.mservices.market"
            def marketBindAddress ="ir.mservices.market.InAppBillingService.BIND"
            manifestPlaceholders = [marketApplicationId: "{marketApplicationId}",
                        marketBindAddress: "${marketBindAddress}",
                    marketPermission: "${marketApplicationId}.BILLING"]
            buildConfigField "String", "IAB_PUBLIC_KEY", “\"{MYKET_PUBLIC_KEY}\""
        }
        otherMarket {
            dimension "store"
            def marketApplicationId = "{OTHER_MARKET_PACKAGE_NAME}"
            def marketBindAddress = "{OTHER_MARKET_BIND_ADDRESS}"
            manifestPlaceholders = [marketApplicationId: "${marketApplicationId}",
                        marketBindAddress  : "${marketBindAddress}",
                        marketPermission   : "{OTHER_MARKET_PERMISSION}"]
            buildConfigField "String", "IAB_PUBLIC_KEY", “\”{OTHER_MARKET_PUBLIC_KEY}\""
        }
    }
    ...
}
برای آشنایی بیشتر با productFlavors می‌توانید اینجا را ببینید.

۴. سپس کلید رمز عمومی (RSA PublicKey) برنامه خود (که از پنل توسعه‌دهندگان مایکت دریافت کردید) را به جای {MYKET_PUBLIC_KEY} جایگزین کنید:

buildConfigField "String", "IAB_PUBLIC_KEY", "\"{MYKET_PUBLIC_KEY}\""

دقت کنید که قسمت buildConfigField پس از تغییر مشابه زیر شده باشد:

buildConfigField "String", "IAB_PUBLIC_KEY", "\"MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgOW5KR56WBWCb5K+yyVDnh/7op0FY4zmM93CWz3xFhgUJe2WXM/8MgpTHiDxrj2Mkgt9bg30qZDtT8gzDHiTgNv6G7pZBDWuyKEariGbbQgoCoeaq3GBcNsQf418jsvOfPjzZ7Rpcl/+9ZPsp1kbJVOmZxnwAZx/wnkUduwfuf8hAgMBAAE=\""

در صورتی که می‌خواهید به غیر از مایکت، استور دیگری را اضافه کنید، باید مقادیر {OTHER_MARKET_PACKAGE_NAME}، {OTHER_MARKET_BIND_ADDRESS}، {OTHER_MARKET_PERMISSION} و {OTHER_MARKET_PUBLIC_KEY} را جایگزین کنید. مثلا برای پیاده‌سازی پرداخت درون‌برنامه‌ای برای استور مایکت و کافه‌بازار، productFlavors به صورت زیر است:

android {
    flavorDimensions "store"
 
    productFlavors {
        myket {
            dimension "store"
            def marketApplicationId = "ir.mservices.market"
            def marketBindAddress ="ir.mservices.market.InAppBillingService.BIND"
            manifestPlaceholders = [marketApplicationId: "{marketApplicationId}",
                        marketBindAddress: "${marketBindAddress}",
                    marketPermission: "${marketApplicationId}.BILLING"]
            buildConfigField "String", "IAB_PUBLIC_KEY", “\"{MYKET_PUBLIC_KEY}\""
        }
        bazaar {
            dimension "store"
            def marketApplicationId = "com.farsitel.bazaar"
            def marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
            manifestPlaceholders = [marketApplicationId: "${marketApplicationId}",
                        marketBindAddress  : "${marketBindAddress}",
                        marketPermission   : "com.farsitel.bazaar.permission.PAY_THROUGH_BAZAAR"]
            buildConfigField "String", "IAB_PUBLIC_KEY", “\”{BAZAAR_PUBLIC_KEY}\""
        }
    }
    ...
}

دقت کنید که در مثال بالا باید قسمت {MYKET_PUBLIC_KEY} با کلید رمز عمومی مایکت و قسمت {OTHER_MARKET_PUBLIC_KEY} با کلید رمز عمومی کافه‌بازار، جایگزین شود. در نهايت می‌توانید برای هر فروشگاه یک خروجی (نسخه) مجزا بگیرید.

۵. کتابخانه مایکت به ساختار کد شما اضافه شد. هم‌اکنون کافی است که پروژهٔ‌ خود را Sync و Rebuild کنید.

مراحل پیاده‌سازی

ساختن IabHelper

برای شروع کار باید یک Instance از IabHelper ایجاد کنیم. با توجه به اینکه در متد‌ها و Callbackهای مختلف نیاز است به این Object دسترسی داشته باشید، بهتر است این Instance را به صورت یک فیلد در Activity خود تعریف کنید. برای ساختن IabHelper نیاز است تا کلید رمز عمومی را (که از پنل توسعه‌دهندگان دریافت کرده‌اید) به همراه Context به Constructor کلاس IabHelper بدهید:

mHelper = new IabHelper(context, BuildConfig.IAB_PUBLIC_KEY);

دقت کنید که مقدار IAB_PUBLIC_KEY (کلید رمز عمومی) را باید بعد از Sync شدن پروژه، از BuildConfig بخوانید.

تنظیم Logger

کلاس IabHelper لاگ‌های خود را با تگ “IabHelper” در خروجی لاگ‌های اندروید (LogCat) می‌نویسد. شما می‌توانید این لاگ‌ها را فعال یا غیر فعال نمایید:

mHelper.enableDebugLogging(true);
توجه کنید که لاگ‌ها را در حالت debug فعال و قبل از انتشار (release) غیر فعال کنید. برای این کار می‌توانید از تنظیمات Gradle استفاده نمایید.

اتصال به سرویس مایکت (startSetup)

برای اتصال به سرویس پرداخت درون‌برنامه‌ای مایکت کافی است از متد startSetup استفاده کنید. این متد را باید قبل از هر متد دیگری صدا بزنید و در صورت متصل شدن به سرویس می‌توانید از سرویس پرداخت درون‌برنامه‌ای مایکت استفاده کنید. حال یک Callback از جنس OnIabSetupFinishedListener در پارامتر‌ این متد پیاده‌سازی کنید:

mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
            public void onIabSetupFinished(IabResult result) {
                 
            }
        });
}

پس از تلاش برای اتصال به سرویس، متد onIabSetupFinished همراه با یک IabResult فراخوانی می‌شود و نتیجه را اعلام می‌کند. در صورتی که خروجی متد isSuccess در IabResult برابر TRUE بود، اتصال با موفقیت انجام شده است. در غیر این صورت برنامهٔ شما نتوانسته به سرویس مایکت متصل شود که در این صورت می‌توانید از متد getMessage برای علت خطا استفاده کنید.

توجه کنید که در تمامی Callbackهای کلاس IabHelper باید به دلیل تاخیر در جواب حتما چک کنید که Instance این Object برابر با Null نشده باشد: if (mHelper == null) return;

به‌روز کردن اطلاعات خرید(queryInventory)

به محض متصل شدن به سرویس مایکت (یعنی زمانی که متد isSuccess در IabResult برابر TRUE بود) باید خرید‌های کاربر را به‌روز کنید. به‌روز کردن خرید کاربر‌ها در مدل‌های محصولات مختلف مفهوم‌های مختلفی دارد:

برای محصولات مصرف‌شدنی این معنی را می‌دهد که به هر علت یک خرید مصرف نشده وجود دارد که باید مصرف شود و به کاربر تحویل داده شود. مثلا فرض کنید که در فرایند خرید قبلی، کاربر با مشکل مواجه می‌شود یا به هر دلیلی قبل از مصرف کردن محصول، برنامهٔ شما بسته می‌شود و یا ارتباط کاربر قطع می‌گردد. در این صورت باید در اجرای بعدی این محصول را مصرف کنید و به کاربر تحول دهید.

علت به‌روز کردن محصولات برای محصولات مصرف‌نشدنی این است که از این طریق متوجه می‌شوید که کاربر محصول شما را خریداری کرده است یا نه. در صورتی که خریداری کرده بود، محصول یا همان امکان مورد نظر (مثلا حذف تبلیغات یا مراحل کامل بازی یا …) را برای او فعال می‌کنید.

در مورد محصولات اشتراکی هم از این طریق متوجه می‌شوید که کاربر هنوز مشترک برنامهٔ شما هست یا خیر. در صورتی که زمان اشتراک تمام نشده بود در queryInventory به شما خبر داده می‌شود.

در واقع می‌توان گفت تمام محصولاتی که مصرف نکرده‌اید،همیشه در queryInventory برمی‌گردد. در صورتی که مصرف‌شدنی بود که باید همان‌جا مصرف شود و بعد تحویل کاربر شود و در صورتی که مصرف‌نشدنی یا اشتراکی بود، باید به کاربر تحویل داده شود. 

همچنین با استفاده از queryInventory می‌توانید اطلاعات مربوط به محصولات خود (نام محصول، توضیحات، قیمت و …) که در پنل توسعه‌دهندگان وارد نموده‌اید را دریافت کنید. مثلا فرض کنید که بعد از انتشار، قیمت یا نام یکی از محصولات خود را تغییر می‌دهید. با استفاده از این قابلیت می‌توانید در UI برنامهٔ خود همیشه مشخصات محصولی را نشان دهید که در پنل توسعه‌دهندگان مایکت قرار دارد. به عبارتی با تغییر آن، برنامه شما نیز تغییر می‌کند.

برای به‌روز کردن خرید‌ها باید نام محصولات را به همراه یک Callback از جنس QueryInventoryFinishedListener به متد queryInventoryAsync از کلاس IabHelper بدهید تا فرایند به‌روز کردن آغاز شود:

try {
    mHelper.queryInventoryAsync(true, itemSkus, subSkus, mGotInventoryListener);
} catch (IabAsyncInProgressException e) {
    Log.e(TAG, "Error querying inventory. Another async operation in progress.");
}

متد queryInventoryAsync دارای چهار پارامتر است که به ترتیب:

۱. boolean querySkuDetails: در صورتی که می‌خواهید اطلاعات محصولات به‌روز شود (نام، قیمت و …) این Boolean را برابر TRUE قرار دهید. توجه کنید در صورتی که نمی‌خواهید از این قابلیت استفاده کنید این مقدار را برابر FALSE قرار دهید تا سرویس اضافه‌ای صدا نشود.

۲. List<String> moreItemSkus: لیستی از ID محصولات فروشی (مصرف‌شدنی و مصرف‌نشدنی). در صورتی که برنامهٔ شما محصولات فروشی ندارد این لیست را برابر NULL قرار دهید.

۳. List<String> moreSubsSkus: لیستی از ID محصولات اشتراکی. در صورتی که برنامه شما محصولات اشتراکی ندارد این لیست را برابر NULL قرار دهید.

۴. QueryInventoryFinishedListener listener: برای جواب این سرویس یک Callback از جنس QueryInventoryFinishedListener تنظیم شود.

توجه کنید که در یک لحظه می‌تواند فقط یک درخواست به سرویس مایکت ارسال کرد. در صورتی که درخواستی در حال اجرا باشد و شما درخواست دیگری را صدا بزنید، خطای IabAsyncInProgressException رخ می‌دهد.

نتیجه این درخواست با دو Object با نام‌های IabResult و Inventory در متد onQueryInventoryFinished بازمی‌گردد. از IabResult موفق بودن/نبودن درخواست مشخص می‌شود و خرید‌های به‌روز شده و اطلاعات محصولات در Inventory قرار می‌گیرد. Inventory شامل دو Map با نام‌های mSkuMap و mPurchaseMap است. خرید‌های به‌روز‌ شده در mPurchaseMap قرار می‌گیرد که با استفاده از متد getPurchase در کلاس Inventory می‌توانید متوجه شوید برای محصول مورد نظرتان خرید به‌روز شده‌ای وجود دارد یا خیر. همچنین می‌توانید با استفاده از متد getAllPurchases به تمام لیستی (از جنس Purchase) درسترسی پیدا کنید.

همچنین می‌توانید اطلاعات خرید که در پنل توسعه‌دهندگان تنظیم کرده‌اید را در mSkuMap بیابید. توجه کنید در صورتی که مقدار querySkuDetails را برابر FALSE قرار دهید این Map برابر با NULL خواهد بود. با استفاده از متد getSkuDetails در کلاس Inventory می‌توانید به اطلاعات خرید محصول مورد نظر دسترسی پیدا کنید و UI برنامهٔ خود را به‌روز نمایید.

مفهوم queryInventory، تجمیع دو مفهوم getPurchases و getSkuDetail است. در واقع با هر queryInventory، یک getPurchases زده می‌شود و در صورتی که فیلد querySkuDetails را TRUE قرار دهید، getSkuDetail هم صدا زده می‌شود. برای آشنایی بیشتر با مفاهیم این APIها ویدئو آموزشی مقدماتی را ببینید.

نمونه‌ای از QueryInventoryFinishedListener را می‌توانید در اینجا ببینید:

IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
 
        // Have we been disposed of in the meantime? If so, quit.
        if (mHelper == null) return;
 
        // Is it a failure?
        if (result.isFailure()) {
            complain("Failed to query inventory: " + result);
            return;
        }
 
        // (1)
        // Do we have the premium upgrade?
        Purchase premiumPurchase = inventory.getPurchase(SKU_PREMIUM);
        Log.d(TAG, "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM"));
         
        // (2)
        // First find out which subscription is auto renewing
        Purchase gasMonthly = inventory.getPurchase(SKU_INFINITE_GAS_MONTHLY);
        Purchase gasYearly = inventory.getPurchase(SKU_INFINITE_GAS_YEARLY);
 
        // (3)
        // Check for gas delivery -- if we own gas, we should fill up the tank immediately
        Purchase gasPurchase = inventory.getPurchase(SKU_GAS);
        consume(SKU_GAS)
        updateUi();
        Log.d(TAG, "Initial inventory query finished; enabling main UI.");
    }
};

همانطور که در مثال بالا می‌بینید در قسمت (1) محصول با شناسهٔ SKU_PREMIUM را از خرید‌های به‌روز شده جست‌وجو می‌کند و در صورتی که premiumPurchase برابر با NULL نباشد این محصول مصرف‌نشدنی به کاربر تحویل می‌شود.

در قسمت (2) برای محصولات اشتراکی جست‌وجو انجام می‌شود و در صورتی که اشتراک ماهیانه یا سالیانه کاربر فعال باشد، مقدار gasMonthly یا gasYearly مخالف NULL خواهد بود.

در قسمت (3) برای محصول مصرف‌شدنی SKU_GAS جست‌وجو انجام می‌شود و در صورت NULL نبودن gasPurchase این محصول مصرف می‌شود (Consume) و سپس به کاربر تحویل داده خواهد شد. در خصوص Consume در ادامه توضیح خواهیم داد.

فیلد‌های آبجکت Purchase به صورت زیر است:

توضیحاتTypeنام
نوع محصول (inapp/subs)StringmItemType
شمارهٔ فاکتور خریدStringmOrderId
پکیج‌نیم برنامهٔ شماStringmPackageName
ID محصولStringmSku
Timestamp زمان خریدlongmPurchaseTime
وضعیت خرید (مصرف‌شده / مصرف نشده)intmPurchaseState
Developer payloadStringmDeveloperPayload
توکن خرید این محصولStringmToken
رشتهٔ JSON که از سرور مایکت برمی‌گرددStringmOriginalJson
Signiture رشتهٔ mOriginalJsonStringmSignature
فعال/غیر‌فعال بودن Auto RenewbooleanmIsAutoRenewing

با توجه به اینکه اولین درخواستی که باید بعد از اتصال به سرویس مایکت صدا زده شود در خواست queryInventoryAsync است، بدنه کلی متد startSetup به صورت زیر خواهد بود (با فرض اینکه لیست‌های subSkus و itemSkus با مقادیر IDهای محصولات ساخته شده است و mGotInventoryListener نیز پیاده‌سازی شده است):

    mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
        public void onIabSetupFinished(IabResult result) {
            Log.d(TAG, "Setup finished.");
 
            if (!result.isSuccess()) {
                // Oh noes, there was a problem.
                complain("Problem setting up in-app billing: " + result);
                return;
            }
 
            // Have we been disposed of in the meantime? If so, quit.
            if (mHelper == null) return;
             
            Log.d(TAG, "Setup successful. Querying inventory.");
            try {
                mHelper.queryInventoryAsync(true, itemSkus, subSkus, mGotInventoryListener);
            } catch (IabAsyncInProgressException e) {
                complain("Error querying inventory. Another async operation in progress.");
            }
        }
    });
}
بسیار مهم: در صورتی که queryInventory را در لحظهٔ شروع برنامه صدا نزنید ممکن است از کاربران برنامهٔ‌ شما مبلغی کاسته شود و هیچگاه، محصول تحویل نگردد. برخی توسعه‌دهندگان برای به‌روز کردن خرید‌ها از دکمه‌ای با نام «به‌روزرسانی خرید‌ها» در برنامهٔ خود استفاده می‌کنند که در آن queryInventory را فراخوانی می‌کنند. توجه کنید که حتی با پیاده‌سازی این دکمه باید در لحظهٔ شروع برنامه خرید‌های کاربر را به‌روز کنید. این مشکل در بین توسعه‌دهندگان سرویس پرداخت درون‌برنامه‌ای بسیار متداول است.

فرستادن درخواست‌های خرید درون‌برنامه‌ای

هنگامی که برنامه شما به مایکت متصل شد، شما می‌توانید برای محصولات درون برنامه درخواست خرید بفرستید. مایکت یک رابط کاربری برای روند پراخت کاربران فراهم می‌کند به طوری که برنامه شما درگیر مدیریت تراکنش‌ها نخواهد شد و این قبیل عملیات‌ بر عهده مایکت خواهد بود. وقتی یک محصول خریداری شد، مایکت کاربر را مالک آن می‌داند و تا زمانی که مصرف نشده، از خرید دوباره آن محصول توسط کاربر جلوگیری می‌کند (یعنی شناسه محصول‌‌هایی که هنوز مصرف نشدند با شناسه محصولاتی که کاربر قصد خرید آن‌ها را دارد، یکی باشد). شما باید نحوه مصرف محصولات در برنامه‌ خود را کنترل کنید و مایکت را برای مصرف مطلع سازید تا محصول دوباره برای خرید مهیا شود. شما همچنین می‌توانید به راحتی فهرست محصولاتی که کاربر مالک آن‌ها است را از مایکت بخواهید. این زمانی مفید خواهد بود که مثلا بخواهید هنگام باز شدن برنامه خود بر اساس این محصولات به کاربر سرویس مناسبی ارائه دهید.

فرض کنید که برنامه شما باز شده و با موفقیت به سرویس مایکت متصل گردیده است. کاربر وارد صفحه فروشگاه برنامه می‌شود و لیستی از محصولات شما را به همراه قیمت‌های آن‌ها مشاهده می‌کند (لیستی که با استفاده از mSkuMap در کلاس Inventory ساخته‌اید). در کنار هر آیتم لیست، یک دکمه خرید وجود دارد. کاربر با زدن دکمه خرید وارد فرایند خرید آن محصول می‌شود. شروع فرایند خرید تمامی محصولات درون‌برنامه‌ای با استفاده از متد launchPurchaseFlow در کلاس IabHelper، به صورت زیر است:

try {
    mHelper.launchPurchaseFlow(this, SKU_PREMIUM, mPurchaseFinishedListener, payload);
} catch (IabAsyncInProgressException e) {
    complain("Error launching purchase flow. Another async operation in progress.");
}

متد launchPurchaseFlow پارامتر‌های زیر را به ترتیب می‌پذیرد:

۱. Activity act: اکتیویتی برنامه شما که فرایند خرید در آن انجام می‌شود.

۲. String sku: شناسه محصول درون‌برنامه‌ای که قصد خرید آن را دارید.

۳. OnIabPurchaseFinishedListener listener: یک Callback از جنس OnIabPurchaseFinishedListener که پایان فرایند خرید را خبر می‌دهد.

۴. String developerPayload: مقدار Developer payload این خرید. از رشته developerPayload برای مشخص کردن هر اطلاعات اضافی که مایل هستید مایکت به همراه اطلاعات خرید برای شما برگرداند، استفاده می‌شود.

زمانی که با استفاده از launchPurchaseFlow فرایند خرید را آغاز می‌کنید، یک Activity از مایکت روی برنامه شما باز می‌شود و مشخصات محصول به همراه درگاه‌های خرید را به کاربر نمایش می‌دهد تا کاربر خرید را انجام دهد.

سپس IabHelper پس از بررسی Signature خرید با کلید رمز عمومی شما، OnIabPurchaseFinishedListener را فراخوانی می‌کند. در متد onIabPurchaseFinished دو Object با نام‌های IabResult و Purchase باز می‌گردد. IabResult نشان می‌دهد که خرید موفق انجام شده است یا خیر و در صورت موفقیت، مشخصات محصول خریداری شده در Purchase قرار داده می‌شود. یک نمونه از پیاده‌سازی این Callback به صورت زیر است:

IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
        Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
 
        // if we were disposed of in the meantime, quit.
        if (mHelper == null) return;
 
        if (result.isFailure()) {
            complain("Error purchasing: " + result);
            setWaitScreen(false);
            return;
        }
 
        Log.d(TAG, "Purchase successful.");
 
        // handling purchase
    }
};

پس از اینکه خرید با موفقیت انجام شد، با توجه به مدل محصول، آن را به کاربر تحویل دهید. اگر محصول برنامه شما مصرف‌نشدنی یا اشتراکی است باید آن را پس از onIabPurchaseFinished موفقیت آمیز به کاربر تحویل دهید. در صورتی که محصول برنامه شما مصرف‌شدنی است ابتدا باید آن را Consume کنید و سپس به کاربر تحویل دهید. Consume کردن یک محصول باعث می‌شود که آن محصول، در لیست queryInventory (محصولات به‌روز شده) بازنگردد، بنابراین محصولات مصرف‌نشدنی و اشتراکی را Consume نکنید.

توصیه امنیتی: هرگاه قصد ارسال یک درخواست خرید داشتید، یک رشته‌ token بسازید که به طور منحصر به فرد این درخواست خرید را مشخص کند و آن را در developerPayload قراد دهید. شما می‌توانید از یک رشته‌ای که تصادفی تولید شده به عنوان token استفاده کنید. هنگامی که شما پاسخ خرید را از مایکت دریافت کردید، مطمئن شوید که امضای دیجیتالی، orderId، و رشته developerPayload داده‌های بازگشتی را بررسی کردید. برای امنیت بیشتر، شما باید این کار را بر روی سرورهای امن خود انجام دهید. اطمینان حاصل کنید که orderId یک مقدار منحصر به فرد باشد و شما قبلا آن را پردازش نکرده باشید و رشته developerPayload با آن token که قبلاً با درخواست خرید فرستاده‌اید مطابقت داشته باشد.

مصرف نمودن یک خرید

هنگامی که یک کاربر محصولی را خریداری می‌کند، مالک آن تلقی می‌شود و امکان خرید دوباره آن را نخواهد داشت. شما باید یک درخواست مصرف به مایکت ارسال کنید تا مایکت اجازه خرید دوباره محصول را به کاربر بدهد.

مهم: تنها محصولات «مصرف شدنی» را می‌توان مصرف کرد، درخواست مصرف برای محصولات «مصرف نشدنی» کد پاسخ خطا برمی‌گرداند. اینکه شما چگونه مصرف شدن محصول در برنامه خود را پیاده می‌کنید به عهده شماست. معمولا شما محصولاتی را مصرف می‌کنید که اثر موقت در برنامه دارند و کاربر ممکن است بخواهد چندین بار آنها را بخرد (برای مثال خرید پول در یک بازی). اما محصولاتی که با یک بار خرید اثر دائمی دارند را مصرف نمی‌کنید (مانند ارتقای حساب کاربری در برنامه). 

برای Consume کردن یک محصول، از متد consumeAsync به همراه یک Purchase و یک Callback از جنس OnConsumeFinishedListener استفاده کنید:

try {
    mHelper.consumeAsync(purchase, mConsumeFinishedListener);
} catch (IabAsyncInProgressException e) {
    complain("Error consuming gas. Another async operation in progress.");
}

یک نمونه از پیاده‌سازی OnConsumeFinishedListener به صورت زیر است:

IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
    public void onConsumeFinished(Purchase purchase, IabResult result) {
        Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
 
 
        // if we were disposed of in the meantime, quit.
        if (mHelper == null) return;
 
        if (result.isSuccess()) {
            // successfully consumed, so we apply the effects of the item in our
            // game world's logic, which in our case means filling the gas tank a bit
            Log.d(TAG, "Consumption successful. Provisioning.");
        }
        else {
            complain("Error while consuming: " + result);
        }
        Log.d(TAG, "End consumption flow.");
    }
};

در متد onConsumeFinished دقیقا مانند onIabPurchaseFinished، یک Purchase برای مشخصات خرید و یک IabResult برای نتیجهٔ Consume برمی‌گردد. در صورتی که isSuccess برابر TRUE بود، می‌توانید محصول را به کاربر تحویل دهید (مثلا سکهٔ او را افزایش دهید).

این مسئولیت شماست که مزایای هر محصول را به کاربر ارائه دهید. به عنوان مثال اگر یک کاربر در بازی شما پول یا سکه خریداری کند، شما باید مقدار پول یا تعداد سکه او را بروز رسانی نمایید.

توصیه امنیتی: شما باید قبل از ارائه مزایای یک محصول مصرف شدنی، یک درخواست مصرف را به مایکت ارسال نمایید و مطمن شوید که مایکت درخواست مصرف را تایید کرده است.

محصولات اشتراکی

مایکت از اشتراک پشتیبانی نمی‌کند. برای اطلاعات بیشتر در این زمینه می‌توانید از تیم پشتیبانی مایکت به آدرس ایمیل developer@myket.ir کمک بگیرید.

ارسال اطلاعات خرید به سرور

در صورتی که تحویل محصولات در منطق برنامه شما سمت سرور انجام می‌شود، لازم است که اطلاعات خرید و کاربر خود را به سرور ارسال کنید. پیشنهاد می‌شود که این کار را پس از انجام موفق Consume در OnConsumeFinishedListener انجام دهید. برای این کار کافی است که اطلاعات خرید را با استفاده از متد getOriginalJson در کلاس Purchase به سرور ارسال کنید. توجه کنید که فیلد token در این Json برای تمام خرید‌های برنامه شما یکتا است و می‌توانید به عنوان کلید از آن استفاده کنید. همچنین در نظر داشته باشید که لازم است حتما در کنار این Json مقدار Signature را نیز (با استفاده از متد getSignature در کلاس Purchase) برای سرور ارسال کنید و با استفاده از کلید رمز عمومی که از پنل توسعه‌دهندگان مایکت دریافت نموده‌اید، با هم تطبیق دهید.

آماده‌سازی جهت بستن برنامه (onDestroy)

در onDestroy اکتیویتی برنامه خود که یک Instance از IabHelper را در آن نگه داشته‌اید، کد‌ زیر را بنویسید:

@Override
public void onDestroy() {
    super.onDestroy();
 
 
    Log.d(TAG, "Destroying helper.");
    if (mHelper != null) {
        mHelper.disposeWhenFinished();
        mHelper = null;
    }
}

برنامه آموزشی TrivialDrive

برنامه آموزشی TrivialDrive توسط گوگل طراحی شده است که ما با تغییرات آن را به سرویس پرداخت درون‌برنامه‌ای مایکت متصل کرده‌ایم. سورس این برنامه را می‌توانید در Github مشاهده نمایید.

در این برنامه چهار دکمه وجود دارد. با استفاده از دکمه سبز رنگ (Drive) می‌توانید بازی کنید و مشاهده نمایید که شاخص بنزین شما کم می‌شود. در صورتی که بنزین شما تمام شود دیگر نمی‌توانید بازی کنید و مجبور به خرید بنزین می‌شوید. با دکمه زرد رنگ (Buy GAS) می‌توانید بنزین بخرید. توجه کنید بنزین در این مثال یک محصول مصرف‌شدنی است. با استفاده از دکمه آبی رنگ (Upgrade my Car) می‌توانید محصول مصرف‌نشدنی بخرید و برای همیشه بنزین داشته باشید و با استفاده از دکمه قرمز رنگ (Get Infinite GAS) می‌توانید اشتراک یک ماهه یا یک ساله بخرید.

ملاحظات امنیتی

برای جلوگیری از دسترسی غیر مجاز به امکانات غیر رایگان برنامهٔ شما لازم است که موارد امنیتی زیر را در نظر بگیرید.

نگه‌داری از محتوا

سعی کنید از سروری برای نگه‌داری محتوای غیر رایگان برنامه خود استفاده کنید و پس از خرید کاربر با استفاده از وب‌سرویس آن را به کلاینت خود منتقل کنید. همچنین پس از دریافت محتوا از سرور، آن را به صورت رمز شده در حافظه دستگاه نگه دارید و از کلیدی منحصر به همان دستگاه برای دسترسی به اطلاعات رمز شده استفاده کنید. در نظر داشته باشید که در صورتی که محتوای برنامهٔ شما درون فایل apk باشد، امن نیست و می‌توان به آن دسترسی داشت.

تایید امضای دیجیتالی روی سرور

برای فرستادن اطلاعات به سِرور برنامه خود، حتما رشته Json را به همراه Signature دریافتی از مایکت (با استفاده از متد getSignature و getOriginalJson در کلاس Purchase) ارسال کنید و فرایند تایید امضای دیجیتال را با کلید رمز عمومی (PublicKey) را در سِرور نیز انجام دهید. توجه کنید فرایند تایید امضای دیجیتال در متد verifyPurchase در کلاس Security انجام می‌شود و دقیقا مانند همین کار را باید در سرور خود نیز انجام دهید:

public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
    if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
            TextUtils.isEmpty(signature)) {
        Log.e(TAG, "Purchase verification failed: missing data.");
        return false;
    }
 
    PublicKey key = Security.generatePublicKey(base64PublicKey);
    return Security.verify(key, signedData, signature);
}

به هم ریختگی کد‌ها

برای جلوگیری از عملکرد برنامه‌های مخرب، حتما از سورس کد برنامهٔ خود محافظت کنید. ابتدا باید در کد خود بهم‌ریختگی (obfuscation) ایجاد کنید. برای این کار می‌توانید از ابزار‌هایی نظیر ProGuard استفاده کنید. همچنین مقادیر حساس نظیر کلید رمز عمومی (Public key) را به صورت رشته‌ای ثابت در کد خود قرار ندهید و سعی کنید این رشته‌ها را در زمان اجرای برنامه بسازید (مانند XOR با چند رشته دیگر). توجه کنید سورس کد‌ کمکی که در اختیار شما قرار دارد می‌تواند در اختیار هکر‌ها نیز قرار گیرد بنابراین بهتر است ساختار پروژه و ترتیب متد‌ها را تغییر دهید تا برنامه شما الگوی متفاوتی داشته باشد و پیدا کردن آن زمان‌بَر شود.

استفاده از Developer payload

در سرویس پرداخت درون‌برنامه‌ای مایکت می‌توانید همراه درخواست خرید یک رشته‌‌، موسوم به developer payload هم ارسال کنید. این رشته می‌تواند به عنوان یک شناسه منحصر به فرد از سمت شما برای این خرید در نظر گرفته شود. بعد از اتمام مراحل خرید این رشته را در کلاس Purchase به برنامهٔ شما بازمی‌گرداند. همچنین زمانی که خرید‌های خود را به‌روز می‌کنید، مایکت این رشته را نیز همراه دیگر جزئیات خرید برمی‌گرداند.
شما باید توکن رشته‌ای استفاده کنید که به برنامه‌ شما در تشخیص کاربری که خرید انجام داده است کمک کند. به این ترتیب بعداً می‌توانید متوجه شوید که خرید مورد نظر برای کاربر معتبر است یا خیر. برای محصولات مصرف‌شدنی این رشته می‌تواند کاملاً تصادفی باشد. اما برای محصولات مصرف‌نشدنی یا اشتراکی، برای اطمینان از صحت خریده شدن محصول توسط کاربر باید از رشته‌ای استفاده کنید که منحصراً آن فرد را شناسایی می‌کند.

وقتی‌ که پاسخ را از مایکت دریافت کردید، مطمئن شوید رشته developer payload که همراه با جزئیات خرید به شما بازگردانده است، همانی است که شما برای شروع عملیات پرداخت ارسال کرده بودید. برای اطمینان از امنیت بیشتر پیشنهاد می‌شود این عملیات اعتبارسنجی را بر روی سِرور خود انجام دهید.

بررسی همیشگی مجوز دسترسی به محتوای برنامه

برای امنیت بیش‌تر پیشنهاد می‌شود که برای محصولات مصرف‌نشدنی و اشتراکی، همیشه با باز شدن برنامه، مجوز دسترسی را از مایکت بپرسید و آن را در هیچ‌جا ذخیره نکنید.

تست پیش از انتشار

برای تست سناریو‌های خرید درون‌برنامه‌ای مایکت تنها کافی است که در پنل توسعه‌دهندگان، Packagename برنامه خود را رزرو کنید یا یک نسخه از apk برنامه را بارگذاری نمایید که به کلید رمز عمومی برنامه خود دسترسی داشته باشید. سپس یک یا چند محصول درون‌برنامه‌ای جهت تست با قیمت بسیار کم (مثلا ۱۰۰ ریال) ایجاد کنید و سناریوهای مختلف را تست نمایید. در نظر داشته باشید برای تست خرید درون‌برنامه‌ای نیاز به انتشار برنامه یا تایید مدیر ندارید.

Was this article helpful?
Dislike 0
بعدی: یونیتی (Unity) با Gradle