پرش به مطلب اصلی

راهنمای پرداخت درون‌برنامه‌ای مایکت در Java و Kotlin

Android Native
پروژه نمونه

برای مشاهده نمونه کامل پرداخت درون‌برنامه‌ای مایکت در اندروید، به پروژه نمونه در GitHub مراجعه کنید.

کتابخانه Myket Billing Client SDK ابزار رسمی اندروید برای پیاده‌سازی پرداخت درون‌برنامه‌ای (IAP) در برنامه‌های توزیع‌شده از طریق فروشگاه مایکت است و از پروژه‌های Kotlin و Java پشتیبانی می‌کند.

قابلیت‌ها​

  • محصولات درون‌برنامه‌ای: پشتیبانی از محصولات مصرف‌شدنی (مانند سکه، بنزین، اعتبار) و محصولات غیرمصرفی (مانند نسخه ویژه و حذف تبلیغات).
  • اعتبارسنجی خودکار امضا: بررسی صحت امضای دیجیتال با کلید عمومی RSA.
  • مکانیزم ارتباط سه‌سطحی: ۱. اتصال IPC / AIDL: اتصال مستقیم به سرویس پرداخت کلاینت مایکت.
    ۲. کانال جایگزین Broadcast Receiver: در شرایط محدودیت اتصال سرویس.
    ۳. پرداخت تحت وب (Chrome Custom Tabs): هدایت به درگاه پرداخت وب مایکت در صورت عدم نصب برنامه مایکت روی دستگاه.
  • پشتیبانی از چند فروشگاه (Multi-Store): ایجاد خروجی برای مارکت‌های مختلف با Product Flavors بدون تغییر کد پرداخت.
  • مدیریت داخلی نتایج: مدیریت داخلی نتیجه اکتیویتی در نسخه ۲.۲ بدون نیاز به پیاده‌سازی دستی onActivityResult.
  • سازگاری با اندروید ۱۱ و بالاتر: ادغام خودکار اعلان‌های دسترسی بسته (<queries>) در مانیفست.

پیش‌نیازها​

  • حداقل نسخه اندروید (Min SDK): اندروید ۵.۰ (API Level 21)
  • نسخه کامپایل (Compile SDK): API Level 34 یا بالاتر
  • جاوا / کاتلین: Java 11 یا بالاتر (پیشنهادی: Java 17)

مراحل نصب و پیکربندی​

۱. افزودن مخزن Maven رسمی مایکت​

مخزن رسمی Maven مایکت را در بخش dependencyResolutionManagement در فایل settings.gradle.kts (یا settings.gradle) اضافه کنید:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://maven.myket.ir")
}
}

۲. افزودن وابستگی کتابخانه​

می‌توانید وابستگی کتابخانه را از طریق Version Catalog (روش توصیه‌شده) یا مستقیماً در فایل build.gradle ماژول برنامه اضافه کنید.

الف) استفاده از Version Catalog (gradle/libs.versions.toml) (پیشنهادی)​

در فایل gradle/libs.versions.toml:

[versions]
myketBilling = "2.2"

[libraries]
myket-billing = { module = "ir.myket:billing", version.ref = "myketBilling" }

سپس در فایل build.gradle.kts ماژول برنامه:

dependencies {
implementation(libs.myket.billing)
}

ب) افزودن مستقیم وابستگی​

dependencies {
implementation("ir.myket:billing:2.2")
}

۳. تنظیم شناسه سرویس و کلید عمومی (Manifest Placeholders)​

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

حالت پیش‌فرض (تک‌فروشگاهی - مایکت)​

تنظیمات زیر را در بخش defaultConfig در فایل build.gradle.kts (یا build.gradle) ماژول برنامه قرار دهید:

android {
defaultConfig {
// ...
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"

// کلید عمومی Base64 دریافت شده از پنل توسعه‌دهندگان مایکت
buildConfigField(
"String",
"IAB_PUBLIC_KEY",
"\"YOUR_BASE64_ENCODED_PUBLIC_KEY\""
)
}
}

مقدار YOUR_BASE64_ENCODED_PUBLIC_KEY را با کلید عمومی برنامه خود که از پنل توسعه‌دهندگان مایکت دریافت کرده‌اید جایگزین کنید.

۴. معماری چندفروشگاهی با Product Flavors​

اگر قصد دارید برنامه خود را علاوه بر مایکت در فروشگاه‌های دیگر منتشر کنید، از productFlavors استفاده کنید:

android {
flavorDimensions += "store"

productFlavors {
create("myket") {
dimension = "store"
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"
buildConfigField("String", "IAB_PUBLIC_KEY", "\"YOUR_MYKET_PUBLIC_KEY\"")
}

create("bazaar") {
dimension = "store"
val marketApplicationId = "com.farsitel.bazaar"
val marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.permission.PAY_THROUGH_BAZAAR"
buildConfigField("String", "IAB_PUBLIC_KEY", "\"YOUR_BAZAAR_PUBLIC_KEY\"")
}
}
}

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

راهنمای گام‌به‌گام پیاده‌سازی​

مرحله اول: مقداردهی اولیه IabHelper و اتصال (startSetup)​

ابتدا در اکتیویتی یا کلاس مدیریت پرداخت، شیء IabHelper را با Context و کلید عمومی مقداردهی کرده و با متد startSetup ارتباط را برقرار کنید:

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import ir.myket.billingclient.IabHelper
import ir.myket.billingclient.util.IabResult
import ir.myket.billingclient.util.Inventory
import ir.myket.billingclient.util.Purchase

class MainActivity : AppCompatActivity() {

private var iabHelper: IabHelper? = null

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

// مقداردهی اولیه Helper
iabHelper = IabHelper(this, BuildConfig.IAB_PUBLIC_KEY).apply {
enableDebugLogging(BuildConfig.DEBUG, "MyketBilling")
}

// برقراری ارتباط با سرویس پرداخت
iabHelper?.startSetup { result ->
if (!result.isSuccess) {
// بروز خطا در اتصال (مثلاً در دسترس نبودن سرویس)
Log.e("Billing", "Setup failed: ${result.message}")
return@startSetup
}

// ارتباط با موفقیت برقرار شد؛ دریافت موجودی و خریدهای فعال
queryInventory()
}
}
}
هشدار

پیش از اطمینان از موفقیت startSetup (بررسی result.isSuccess)، نباید هیچ‌کدام از متدهای خرید یا استعلام موجودی را فراخوانی کنید.

مرحله دوم: دریافت موجودی و خریدهای قبلی (queryInventory)​

بلافاصله پس از اتصال موفق و در ابتدای اجرای برنامه، باید فهرست محصولات و وضعیت خریدهای فعال کاربر را استعلام بگیرید. این مرحله برای اهداف زیر حیاتی است:

۱. محصولات مصرف‌شدنی (Consumable): اگر کاربر خریدی انجام داده باشد اما پیش از مصرف و تحویل آن، برنامه بسته شده یا کرش کرده باشد، این خرید در فهرست خریدهای فعال باقی می‌ماند. شما باید این خرید را شناسایی، مصرف و تحویل کاربر دهید.
۲. محصولات غیرمصرفی (Non-consumable): بررسی مالکیت محصولاتی مثل «نسخه حرفه‌ای» و فعال‌سازی مجدد آن برای کاربر.
۳. اطلاعات محصولات (SKU Details): دریافت عنوان، توضیحات و قیمت لحظه‌ای ثبت‌شده در پنل توسعه‌دهندگان مایکت.

private fun queryInventory() {
val skuList = listOf("premium_upgrade", "coin_pack_100")

iabHelper?.queryInventoryAsync(true, skuList) { result, inventory ->
if (result.isFailure || inventory == null) {
Log.e("Billing", "Failed to query inventory: ${result.message}")
return@queryInventoryAsync
}

// ۱. بررسی مالکیت محصول غیرمصرفی
val premiumPurchase = inventory.getPurchase("premium_upgrade")
val isPremium = premiumPurchase != null && verifyDeveloperPayload(premiumPurchase)

// ۲. بررسی و مصرف خریدهای مصرفی پردازش‌نشده قبلی
val coinPurchase = inventory.getPurchase("coin_pack_100")
if (coinPurchase != null && verifyDeveloperPayload(coinPurchase)) {
consumeProduct(coinPurchase)
}

// به‌روزرسانی رابط کاربری
updateUi(isPremium)
}
}

فیلدهای کلاس Purchase​

شیء Purchase اطلاعات جزئی تراکنش را فراهم می‌کند:

نام متد / فیلدنوع دادهتوضیحات
getItemType()Stringنوع خرید (inapp یا subs)
getOrderId()Stringشناسه یکتای سفارش مایکت
getPackageName()Stringشناسه بسته برنامه خریدار
getSku()Stringشناسه محصول خریداری‌شده
getPurchaseTime()longزمان انجام تراکنش به‌صورت Timestamp (میلی‌ثانیه)
getPurchaseState()intوضعیت خرید (۰: موفق/خریداری‌شده)
getDeveloperPayload()Stringاطلاعات تکمیلی ارسال‌شده توسط برنامه در زمان شروع خرید
getToken()Stringتوکن یکتای خرید جهت اعتبارسنجی سمت سرور و مصرف خرید
getOriginalJson()Stringمتن کامل داده خام خرید با فرمت JSON
getSignature()Stringامضای دیجیتال داده‌ها جهت اعتبارسنجی با کلید عمومی

مرحله سوم: شروع فرآیند خرید (launchPurchaseFlow)​

برای شروع تراکنش، دیالوگ پرداخت مایکت را فراخوانی کنید.

عدم نیاز به onActivityResult در نسخه ۲.۲

در کتابخانه Myket Billing Client 2.2، نیازی به بازنویسی یا ارسال نتیجه در onActivityResult اکتیویتی خود ندارید. SDK از اکتیویتی پروکسی شفاف داخلی برای دریافت نتیجه استفاده می‌کند و نتیجه مستقیماً به Callback شما ارسال می‌شود.

private fun buyItem(sku: String) {
val developerPayload = generateSecurePayload()

iabHelper?.launchPurchaseFlow(
this,
sku,
{ result, purchase ->
if (result.isFailure || purchase == null) {
Log.e("Billing", "Purchase failed: ${result.message}")
return@launchPurchaseFlow
}

if (!verifyDeveloperPayload(purchase)) {
Log.e("Billing", "Authenticity verification failed.")
return@launchPurchaseFlow
}

// مدیریت خرید موفق با توجه به نوع محصول
when (purchase.sku) {
"coin_pack_100" -> consumeProduct(purchase)
"premium_upgrade" -> unlockPremiumFeature(purchase)
}
},
developerPayload
)
}

مرحله چهارم: مصرف محصول خریداری‌شده (consumeAsync)​

محصولات مصرف‌شدنی (مانند سکه، اعتبار، بسته‌های مصرفی) پس از خرید باید حتماً مصرف (consume) شوند تا کاربر بتواند در آینده مجدداً آن‌ها را خریداری کند:

private fun consumeProduct(purchase: Purchase) {
iabHelper?.consumeAsync(purchase) { consumedPurchase, result ->
if (result.isSuccess) {
// اعطای اعتبار به حساب کاربر
creditUserCoins(100)
Log.d("Billing", "Product successfully consumed: ${consumedPurchase.sku}")
} else {
Log.e("Billing", "Consumption failed: ${result.message}")
}
}
}
اخطار مهم

محصولات غیرمصرفی (Non-consumable مانند «ارتقا به نسخه طلایی») را هرگز مصرف نکنید. مصرف کردن یک محصول، آن را از لیست خریدهای فعال حساب کاربر در مایکت حذف می‌کند و مالکیت آن از بین می‌رود.

مرحله پنجم: آزادسازی منابع (onDestroy)​

جهت جلوگیری از نشت حافظه (Memory Leak) و آزادسازی اتصال سرویس، در زمان بسته‌شدن Activity یا ViewModel، منابع IabHelper را آزاد کنید:

override fun onDestroy() {
super.onDestroy()
iabHelper?.dispose()
iabHelper = null
}

نمونه کامل کد جاوا (MainActivity.java)​

نمونه کامل پیاده‌سازی گام‌به‌گام چرخه پرداخت در جاوا:

package com.example.myketbilling;

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import ir.myket.billingclient.IabHelper;
import ir.myket.billingclient.util.Purchase;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MyketBilling";
private IabHelper mHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mHelper = new IabHelper(this, BuildConfig.IAB_PUBLIC_KEY);
mHelper.enableDebugLogging(BuildConfig.DEBUG, TAG);

mHelper.startSetup(result -> {
if (!result.isSuccess()) {
Log.e(TAG, "Setup error: " + result.getMessage());
return;
}

mHelper.queryInventoryAsync(true, Arrays.asList("gas", "premium"), (invResult, inv) -> {
if (invResult.isSuccess() && inv != null) {
Purchase gas = inv.getPurchase("gas");
if (gas != null) {
mHelper.consumeAsync(gas, (purchase, res) -> {
if (res.isSuccess()) {
Log.d(TAG, "Gas consumed and provisioned.");
}
});
}

Purchase premium = inv.getPurchase("premium");
if (premium != null) {
Log.d(TAG, "User owns premium access.");
}
}
});
});
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.dispose();
mHelper = null;
}
}
}

نکات امنیتی و توصیه‌ها​

۱. محافظت از کلید عمومی (Public Key):
از قرار دادن کلید عمومی به‌صورت متن ساده در سورس‌کدهای عمومی گیت‌هاب خودداری کنید. می‌توانید کلید را در زمان اجرا بسازید یا در لایه Native با C++/NDK نگهداری کنید.
۲. اعتبارسنجی سمت سرور (Server-Side Validation):
برای بازی‌ها و برنامه‌های دارای تراکنش مالی مهم، مقادیر purchase.getOriginalJson() و purchase.getSignature() را به سرور خود ارسال کنید و با کلید عمومی مایکت یا API مایکت اعتبارسنجی کنید.
۳. جلوگیری از تحویل تکراری با توکن خرید:
توکن خرید (purchase.getToken()) شناسه‌ای یکتا است. وضعیت هر توکن را در دیتابیس ثبت کنید تا با ارسال مجدد درخواست، اعتبار تکراری داده نشود.
۴. توالی مصرف و تحویل کالا:
برای محصولات مصرفی، توصیه می‌شود ابتدا کالای دیجیتال را پس از موفقیت consumeAsync تحویل دهید؛ یا در صورتی که قبل از مصرف تحویل می‌دهید، وضعیت محلی پایداری داشته باشید تا در شروع مجدد برنامه، خریدهای مصرف‌نشده را مجدداً به اشتباه تحویل ندهید و فقط Consume را کامل کنید.
۵. استفاده از Developer Payload:
هنگام فراخوانی خرید، رشته‌ای امن (مثلاً شناسه کاربر + هش تراکنش) در developerPayload ارسال کنید و در زمان دریافت نتیجه بررسی کنید که خرید متعلق به همان کاربر باشد.

نکته

توجه کنید که مقدار طول رشته‌ی developerPayload نمی‌تواند بیش‌تر از ۸۰۰ کاراکتر باشد.

تنظیمات ProGuard / R8​

در صورتی که در پروژه خود از فشرده‌سازی و مبهم‌سازی کد (R8 / ProGuard) استفاده می‌کنید، قوانین زیر را در فایل proguard-rules.pro قرار دهید:

# Myket Billing Client
-keep class ir.myket.billingclient.** { *; }
-keep interface ir.myket.billingclient.** { *; }
-keep class com.android.vending.billing.** { *; }

# Models & Reflection
-keepclassmembers class ir.myket.billingclient.util.** {
public <fields>;
public <methods>;
}

پروژه نمونه و آزمایش پرداخت​

یک برنامه نمونه کامل و کاربردی شامل محصولات مصرف‌شدنی (gas) و غیرمصرفی (premium) در مخزن گیت‌هاب رسمی مایکت در دسترس است:

پشتیبانی از اشتراک

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

چک‌لیست آزمایش پیش از انتشار عمومی​

برای آزمایش نیازی به تأیید نهایی یا انتشار عمومی برنامه در مایکت ندارید: ۱. بسته برنامه را در پنل توسعه‌دهندگان مایکت تعریف کنید.
۲. کلید عمومی را در پروژه قرار دهید.
۳. چند محصول آزمایشی با قیمت کم تعریف کنید.
۴. آخرین نسخه مایکت را روی دستگاه فیزیکی یا شبیه‌ساز نصب کنید.
۵. سناریوهای زیر را تست کنید:

  • خرید موفق محصول مصرفی و غیرمصرفی
  • لغو خرید توسط کاربر
  • بستن ناگهانی برنامه هنگام پرداخت و بازیابی آن با queryInventory در شروع مجدد
  • عدم امکان خرید مجدد محصول مصرفی پیش از consume
  • رفتار برنامه در صورت عدم نصب بودن کلاینت مایکت (تست Web Payment Fallback)