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

کافه‌بازار واسط ساد‌ه و کارایی برای مدیریت پرداخت‌ درون‌برنامه‌ای ارائه می‌دهد. اطلاعات زیر نحوهٔ ارسال درخواست از برنامهٔ شما به سرویس پرداخت درون‌برنامه‌ای توسط API را نشان می‌دهد.

توجه: برای پیاده‌سازی کامل و یادگیری چگونگی‌ تست پرداخت‌های درون‌برنامه‌ای پروژهٔ TrivalDrive را مشاهده کنید. این پروژهٔ مثال کاملی از پیاده‌سازی پرداخت درون‌برنامه‌ای ارائه می‌کند که شامل کلاس‌هایی برای انجام وظایف کلیدی مربوط به برقراری اتصالال، فرستادن درخواست پرداخت، پردازش پاسخ در بازار و مدیریت موازی کارهای پس‌زمینه‌ای است تا شما بتوانید فراخوانی متدهای پرداخت درون‌برنامه‌ای را از activity اصلی خود انجام دهید.

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

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

اضافه کردن فایل AIDL به پروژه

فایل Android Interface Definition Language (AIDL)‎ اینترفیسی برای سرویس پرداخت درون‌برنامه‌ای بازار است. این فایل را می‌توانید در پروژهٔ TrivialDrive پیدا کنید. وقتی‌ شما این فایل را به پروژهٔ خود اضافه ‌می‌کنید، محیط توسعهٔ اندروید شما، یک فایل اینترفیس (IInAppBillingService.java) می‌سازد. شما می‌توانید از این اینترفیس استفاده کنید تا درخواست پرداخت را با استفاده از فراخوانی‌های IPC بسازید.

افزودن کتابخانهٔ پرداخت درون‌برنامه‌ای به پروژه:

  1. فایل IInAppBillingService.aidl را در پروژهٔ اندروید خود کپی‌ کنید.
    • اگر از Eclipse استفاده می‌کنید: فایل IInAppBillingService.aidl را در دایرکتوری /src import کنید. Eclipse به طور خودکار فایل اینترفیس را بعد از اینکه پروژهٔ خود را build کردید در پروژه شما می‌سازد.
    • اگر از محیطی‌ به جز Eclipse استفاده می‌کنید: پوشه /src/com/android/vending/billing را ساخته٬ سپس فایل IInAppBillingService.aidl را در این پوشه کپی‌ کنید. فایل AIDL را در پروژه خود قرار داده و از ابزار Ant برای build کردن پروژه خود استفاده کنید. پس از آن فایل IInAppBillingService.java ساخته خواهد شد.
  2. برنامه خود را build کنید. باید فایل ساخته شده‌ای با نام IInAppBillingService.java در پوشه /gen پروژه خود داشته باشید.

manifest برنامه خود را ارتقا دهید

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

برای اینکه به برنامهٔ خود دسترسی‌های موردنیاز را بدهید، کد زیر را به فایل AndroidManifest.xml اضافه کنید:

<uses-permission android:name="com.farsitel.bazaar.permission.PAY_THROUGH_BAZAAR"></uses-permission>

ساخت ServiceConnection

برنامهٔ شما باید از طریق یک ServiceConnection با بازار ارتباط برقرار کند:

  • وصل شدن به IInAppBillingService
  • .
  • فرستادن درخواست پرداخت (در قالب فراخوانی متد IPC) به برنامه بازار.
  • مدیریت پیام‌های پاسخی که با هر درخواست پرداخت فرستاده می‌شود.

اتصال به IInAppBillingService

برای برقراری ارتباط با سرویس پرداخت درون‌برنامه‌ای بازار، ServiceConnection را برای اتصال activity خود به IInAppBillingService پیاده‌سازی کنید. متدهای onServiceDisconnected و onServiceConnected را override کنید تا پس از برقراری اتصال به نمونه‌ای از IInAppBillingService ریفرنسی داشته باشید.

IInAppBillingService mService;

ServiceConnection mServiceConn = new ServiceConnection() {
   @Override
   public void onServiceDisconnected(ComponentName name) {
       mService = null;
   }

   @Override
   public void onServiceConnected(ComponentName name, 
      IBinder service) {
       mService = IInAppBillingService.Stub.asInterface(service);
   }
};

در متد onCreate از activity خود، اتصال را با فراخوانی متد bindService برقرار کنید. به این متد، دو ورودی بدهید: یکی Intentای که رفرنسی به سرویس پرداخت درون‌برنامه‌ای دارد و دیگری نمونه‌ای از ServiceConnectionای که ایجاد نموده‌اید.

شما اکنون می‌توانید از رفرنسی که در mService ساخته‌شده‌است، برای ارتباط با سرویس بازار استفاده کنید.

@Override
public void onCreate(Bundle savedInstanceState) {    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);        
    bindService(new 
        Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND"),
                mServiceConn, Context.BIND_AUTO_CREATE);

مهم: به یاد داشته باشید که اتصال از سرویس پرداخت درون‌برنامه‌ای را زمانی‌ که activity تان را می‌بندید، قطع کنید. اگر اتصال را قطع نکنید، اتصال باز به سرویس باعث تنزل کارایی دستگاه کاربر می‌شود. مثال زیر طریقهٔ قطع کردن اتصال در سرویس پرداخت درون‌برنامه‌ای با نام mServiceConn را نشان می‌دهد که با overrideکردن متد onDestroy در activity انجام می‌شود.

@Override
public void onDestroy() {
    super.onDestroy();
    if (mServiceConn != null) {
        unbindService(mServiceConn);
    }   
}

برای پیاده‌سازی کامل اتصال به یک سرویس که به IInAppBillingService متصل می‌شود، پروژهٔ TrivalDrive را مشاهده کنید.

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

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

جستجو برای محصولات قابل خرید

در برنامه‌تان می‌توانید جزئیات محصول را از بازار دریافت کنید. برای این کار نخست Bundleی را که شامل ArrayListی از رشته‌های شناسهٔ محصولات با کلید "ITEM_ID_LIST" است بسازید، که هر رشتهٔ شناسه‌ٔ محصول برای یک محصولِ قابل خرید است.

ArrayList skuList = new ArrayList();
skuList.add("premiumUpgrade");
skuList.add("gas");
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(“ITEM_ID_LIST”, skuList);

برای بازیابی این اطلاعات از بازار، متد getSkuDetails را فراخوانی کنید و به این متد، ورودی‌های نسخهٔ API پرداخت درون‌برنامه‌ای ("3")، نام بستهٔ برنامهٔ خود، نوع خرید("inapp") و Bundleی که ساختید را بدهید.

Bundle skuDetails = mService.getSkuDetails(3, 
   getPackageName(), "inapp", querySkus);

اگر درخواست موفقیت‌آمیز بود، خروجی Bundle کد پاسخ BILLING_RESPONSE_RESULT_OK (0) را خواهد داشت.

اخطار: متد getSkuDetails را در Thread اصلی (Thread رابط کاربری) برنامه‌تان فراخوانی نکنید. فراخوانی این متد باعث ایجاد درخواستی شبکه‌ای می‌شود که نباید در Thread اصلی (Thread رابط کاربری) برنامهٔ شما انجام شود و آن را بلوکه کند. در عوض یک Thread جداگانه بسازید و متد getSkuDetail را از درون آن فراخوانی کنید.

برای دیدن تمامی‌ کدهای پاسخ بازار API Reference را مشاهده کنید.

نتایج پرس‌و‌جو در یک رشته با کلید DETAILS_LIST و اطلاعات خرید در یک رشته با فرمت JSON ذخیره شده است. برای دیدن اطلاعات جزئیات محصول که پاسخ داده می‌شود، API Reference را مشاهده کنید.

int response = skuDetails.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList responseList 
      = skuDetails.getStringArrayList("DETAILS_LIST");
   
   for (String thisResponse : responseList) {
      JSONObject object = new JSONObject(thisResponse);
      String sku = object.getString("productId");
      String price = object.getString("price");
      if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price;
      else if (sku.equals("gas")) mGasPrice = price;
   }
}

خرید محصول

برای شروع درخواست خرید، متد getBuyIntent را از سرویس پرداخت درون‌برنامه‌ای فراخوانی کنید. به این متد ورودی‌های نسخه‌ٔ API پرداخت درون‌برنامه‌ای ("3")، نام بسته برنامه‌‌تان، شناسهٔ محصول قابل خریداری، نوع خرید(inapp”") و رشتهٔ developerPayload را بدهید. رشتهٔ developerPayload برای مشخص کردن هرگونه آرگومان اضافی که از بازار می‌خواهید همراه اطلاعات خرید برای شما فرستاده‌ شود، استفاده می‌شود.

Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(),
   sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");

اگر درخواست موفقیت آمیز بود، Bundle برگشتی‌ کد پاسخ BILLING_RESPONSE_RESULT_OK و PendingIntentی که برای شروع جریان خرید می‌توانید از آن استفاده کنید را به همراه دارد. برای دیدن تمامی‌ کدهای پاسخ که ممکن از بازار فرستاده شود API Reference را مشاهده کنید. سپس یک PendingIntent از پاسخ Bundle با کلید BUY_INTENT استخراج کنید.

PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");

برای تکمیل تراکنش خرید، متد startIntentSenderForResult را فراخوانی کنید و از PendingIntentی که خودتان ساخته‌اید، استفاده کنید. در این مثال شما از مقدار دلخواه 1001 برای کد درخواست استفاده می‌کنید.

startIntentSenderForResult(pendingIntent.getIntentSender(),
   1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
   Integer.valueOf(0));

بازار پاسخ آن PendingIntent را به متد onActivityResult برنامهٔ شما می‌فرستد. متد onActivityResult با کد نتیجه Activity.RESULT_OK یا Activity.RESULT_CANCELED فراخوانی خواهد شد. به همراه این کد، Intentی خواهد بود که نقش حوالهٔ دیجیتالی خرید شما را خواهد داشت. این حواله در فرمت JSON است و با کلید INAPP_PURCHASE_DATA در پاسخ Intent قرار دارد. به عنوان مثال:

'{ 
   "orderId":"rojeslcdyyiapnqcynkjyyjh", 
   "packageName":"com.example.app",
   "productId":"exampleSku",
   "purchaseTime":1345678900000,
   "purchaseState":0,
   "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   "purchaseToken":"rojeslcdyyiapnqcynkjyyjh"
 }'

برای دیدن فهرست کامل کلیدهای موجود در پاسخ برگردانده شده API Reference را مشاهده کنید.

در ادامهٔ مثال قبل، شما کد پاسخ ، دادهٔ خرید و امضای پاسخ Intent را دریافت می‌کنید.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == 1001) {           
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
        
      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);
            String sku = jo.getString("productId");
            alert("You have bought the " + sku + ". Excellent choice, 
               adventurer!");
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}
پیشنهاد امنیتی: وقتی‌ درخواست خرید را می‌فرستید، یک رشتهٔ token که به طور مشخص این درخواست خرید را بشناسد و شامل این token در developerPayload باشد، ایجاد کنید. شما می‌توانید از رشته‌هایی که به طور تصادفی ساخته می‌شوند به عنوان token استفاده کنید. وقتی‌ پاسخ درخواست خرید را از بازار دریافت کردید، حتما امضای داده‌های برگشتی‌، orderId و رشته developerPayload را بررسی کنید. برای امنیت بیشتر، این کار را در سرور امن خود نیز انجام دهید. مطمئن شوید که مقدارorderId منحصر به فرد است و شما قبلا پردازش نکرده‌اید و رشتهٔ developerPayload با tokenی که شما قبلا با درخواست خرید فرستاده بودید، مطابقت دارد.

نکته‌ای مهم

برخی اوقات، ممکن است بعد از اینکه کاربر را به سیستم پرداخت درون‌برنامه‌ای منتقل نمودید، Activity برنامهٔ شما به صورت خودکار توسط اندروید بسته شود. با این‌که پس از اتمام کار کاربر با سیستم پرداخت، اندروید به صورت خودکار دوباره Activity برنامهٔ شما را می‌سازد، ولی تغییراتی که شما در Activity مورد نظر داده‌اید از بین خواهد رفت. لذا این اتفاق باعث می‌شود که کاربر خرید را انجام دهد ولی برنامهٔ شما نتواند آن‌را ثبت کند. این اتفاق روی دستگاه‌های ضعیف اندرویدی به کرات و در گوشی‌های قوی هم گاهی اتفاق می‌افتد. برای جلوگیری از این کار، متد onSaveInstanceState را برای Activityای که کاربر را به سیستم پرداخت می‌فرستد پیاده کنید و هر چیزی که می‌خواهید پس از بسته‌شدن Activity باز یابید، در Bundleای که به عنوان آرگومان به متد گفته شده داده شده است، ست کنید. پس از بسته شدن Activity، و باز شدن مجدد آن توسط اندروید، همین Bundleای که در متد onSaveInstanceState تغییرش دادید، به عنوان آرگومان متد onCreate در Activity گفته شده داده خواهد شد، که می‌توانید اطلاعات ذخیره‌شده‌تان را بازیابی کنید. برای اطلاعات بیشتر به اینجا سر بزنید.

جستجو در محصولات خریداری شده

برای بازیابی اطلاعات خرید کاربر از برنامه‌تان، متد getPurchases را فراخوانی کنید. به عنوان ورودی، شماره نسخهٔ API درون برنامه‌ای ("3")، نام بسته برنامه‌تان و نوع خرید ("inapp") را بدهید.

Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);

اگر درخواست موفقیت آمیز باشد، Bundle برگشتی‌ کد پاسخ 0 را خواهد داشت. پاسخ Bundle همچنین شامل لیستی از شناسهٔ محصولات، لیستی از جزئیات سفارش هر خرید و امضای هر خرید است.

برای افزایش کارایی، سرویس پرداخت درون‌ برنامه‌ای، زمانی که برای اولین بار getPurchases فراخوانیمی‎شود، فقط تا ۱۰۰ محصول را که متعلق به کاربر است باز می‌گرداند. اگر کاربر تعداد زیادی محصول خریداری شده داشته باشد، بازار رشتهٔ token که به کلید INAPP_CONTINUATION_TOKEN ، منطبق شده است را در پاسخ Bundle قرار می‌دهد تا نشان دهد محصولات بیشتری می‌توانند بازیابی شوند. سپس برنامه شما getPurchases را فراخوانی می‌کند و این token را به عنوان آرگومان به آن می‌دهد. بازار برگرداندن یک token دنباله‌دار در Bundle پاسخ را تا زمانی که تمام محصولاتی که متعلق به کاربر است به برنامه شما فرستاده شود، ادامه می‌دهد.

برای اطلاعات بیشتر در مورد داده‌هایی که توسط getPurchases بازگردانده می‌شود، API Reference را مشاهده کنید. مثال زیر نشان می‌دهد که چگونه شما می‌توانید این داده را دریافت کنید.

int response = ownedItems.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList ownedSkus = 
      ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList purchaseDataList = 
      ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList signatureList = 
      ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
   String continuationToken = 
      ownedItems.getString("INAPP_CONTINUATION_TOKEN");
   
   for (int i = 0; i < purchaseDataList.size(); ++i) {
      String purchaseData = purchaseDataList.get(i);
      String signature = signatureList.get(i);
      String sku = ownedSkus.get(i);
  
      // do something with this purchase information
      // e.g. display the updated list of products owned by user
   } 

   // if continuationToken != null, call getPurchases again 
   // and pass in the token to retrieve more items
}

مصرف خرید

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

برای ذخیره مصرف خرید، متد consumePurchase را فراخوانی کنید و مقدار رشتهٔ purchaseToken را به آن بدهید که خریدی را مشخص می‌کند که بنا بر مصرف آن است. در نتیجه خرید موفق، purchaseToken قسمتی از داده‌ای است که در رشته INAPP_PURCHASE_DATA توسط بازار برگردانده می‌شود. در این مثال شما مصرف محصولی را که با purchaseToken در متغیر token مشخص می‌شود، ذخیره می‌کنید.

int response = mService.consumePurchase(3, getPackageName(), token);

اخطار: متد consumePurchase را در thread اصلی (thread رابط کاربری) فراخوانی نکنید. فراخوانی کردن این متد باعث ایجاد درخواستی شبکه‌ای می‌شود که thread اصلی شما (thread رابط کاربری) را بلوکه می‌کند. به جای آن یک thread جداگانه بسازید و متد consumePurchase را از درون آن فراخوانی کنید.

مسئولیت شما تأمین کالای خریداری شده در درون برنامه است.. به عنوان مثال، اگر کاربر سکهٔ درون بازی‌ را بخرد، شما باید فهرست اموال بازیکن را با مقدار سکه‌ای که خریده است، به روز کنید.

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

پیاده‌سازی اشتراک

روند پیاده‌سازی اشتراک مانند روند پیاده‌سازی خرید محصول است با این تفاوت که نوع محصول باید برابر با subs باشد. پاسخ و نتیجهٔ خرید دقیقا همانند خرید محصولات درون‌برنامه‌ای٬ به متد onActivityResult شما ارسال می‌شود.

Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
   MY_SKU, "subs", developerPayload);

PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
   // Result will be delivered through onActivityResult().
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}

برای بازیابی اشتراک‌های فعال٬ بار دیگر از متد getPurchases استفاده کنید با این تفاوت که پارامتر مربوط به نوع محصول را برابر با subs قرار دهید.

Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
                   "subs", continueToken);

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

امن کردن برنامه‌تان

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

شما می‌توانید بررسی امضای گفته شده را بر روی برنامه خود انجام دهید، آن‌طور که در پروژهٔ TrivalDrive انجام شده است. ولی چنانچه برنامه‌تان برای ارائهٔ امکاناتش به سروری امن اتصال پیدا می‌کند، پیشنهاد ما انجام این بررسی بر روی آن سرور امن است.

برای کسب اطلاعات بیشتر درباره طراحی‌های امنیتی به بخش ملاحظات امنیتی مراجعه کنید.