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

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

Flutter
پروژه نمونه

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

پلاگین myket_iap ابزار رسمی برای پیاده‌سازی پرداخت درون‌برنامه‌ای (In-App Billing) مایکت در برنامه‌های فلاتر است. این افزونه امکان راه‌اندازی سرویس پرداخت، استعلام محصولات و موجودی، شروع فرآیند خرید و مدیریت محصولات مصرفی و غیرمصرفی را فراهم می‌کند.

نکته: این پلاگین فقط از سیستم‌عامل اندروید پشتیبانی می‌کند.

قابلیت‌ها​

  • راه‌اندازی با حداقل پیکربندی: پیکربندی ساده در پروژه اندروید فلاتر.
  • پشتیبانی از انواع محصولات: مدیریت خریدهای یک‌باره شامل محصولات مصرف‌شدنی (مانند سکه یا اعتبار) و محصولات غیرمصرفی (مانند ارتقا به نسخه ویژه یا حذف تبلیغات).
  • استعلام موجودی و قیمت‌گذاری: دریافت فهرست خریدهای فعال کاربر و مشخصات محصول (قیمت ریالی، عنوان و توضیحات) از مایکت.
  • پشتیبانی از چند فروشگاه (Multi-Store): تغییر فروشگاه در زمان بیلد با استفاده از Gradle Product Flavors و مشخصه‌های مانیفست بدون تغییر کد Dart.
  • مدیریت خطای استاندارد: ارائه کدهای نتیجه استاندارد به همراه پیام‌های توصیفی خطا.
  • سازگاری با نسخه‌های جاری: سازگار با Flutter 3+، Android Gradle Plugin 8+، Kotlin 2+ و Android SDK 34+.

پیش‌نیازها​

۱. حساب کاربری در پنل توسعه‌دهندگان مایکت.
۲. ثبت برنامه در پنل مایکت و رزرو شناسه بسته (Package Name).
۳. دریافت کلید عمومی RSA پرداخت برنامه از پنل توسعه‌دهندگان مایکت.
۴. تعریف شناسه‌های کالا (SKU) در بخش پرداخت پنل برنامه.
۵. نصب برنامه مایکت روی دستگاه یا شبیه‌ساز تست.

مراحل نصب​

وابستگی myket_iap را به پروژه فلاتر اضافه کنید:

flutter pub add myket_iap

یا آن را مستقیماً در فایل pubspec.yaml قرار دهید:

dependencies:
flutter:
sdk: flutter
myket_iap: ^2.0.1

سپس دستور زیر را اجرا کنید:

flutter pub get

پیکربندی بخش اندروید (Android Setup)​

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

کتابخانه پرداخت بومی مایکت در مخزن اختصاصی مایکت میزبانی می‌شود. آدرس https://maven.myket.ir را به مخازن اندروید اضافه کنید:

در پروژه‌های مدرن فلاتر (android/settings.gradle.kts):​

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

در ساختار Groovy DSL (android/build.gradle یا settings.gradle):​

allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://maven.myket.ir' }
}
}

۲. تنظیم مشخصه‌های جایگزین در مانیفست (Manifest Placeholders)​

مشخصه‌های سرویس مایکت را به defaultConfig در فایل android/app/build.gradle.kts یا android/app/build.gradle اضافه کنید:

در فایل android/app/build.gradle.kts (Kotlin DSL):​

android {
...
defaultConfig {
...
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders.putAll(
mapOf(
"marketApplicationId" to marketApplicationId,
"marketBindAddress" to marketBindAddress,
"marketPermission" to "$marketApplicationId.BILLING"
)
)
}
}

در فایل android/app/build.gradle (Groovy DSL):​

android {
...
defaultConfig {
...
def marketApplicationId = "ir.mservices.market"
def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders += [
marketApplicationId: "${marketApplicationId}",
marketBindAddress : "${marketBindAddress}",
marketPermission : "${marketApplicationId}.BILLING"
]
}
}

۳. پشتیبانی از چند فروشگاه (Multi-Store & Product Flavors)​

در صورت انتشار برنامه در چندین فروشگاه اندرویدی، می‌توانید در android/app/build.gradle یا build.gradle.kts از Product Flavors استفاده کنید:

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"
]
}
bazaar {
dimension "store"
def marketApplicationId = "com.farsitel.bazaar"
def marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: "${marketApplicationId}",
marketBindAddress : "${marketBindAddress}",
marketPermission : "${marketApplicationId}.permission.PAY_THROUGH_BAZAAR"
]
}
}
}

۴. راهنمای مهاجرت از نسخه‌های ۱.۰.x به ۱.۱.۰ و بالاتر​

اگر در حال به‌روزرسانی از نسخه 1.0.6 یا قبل‌تر هستید:

۱. حذف مجوز دستی از فایل android/app/src/main/AndroidManifest.xml:

<!-- این خط را حذف کنید -->
<uses-permission android:name="ir.mservices.market.BILLING" />

۲. حذف بلوک <queries> از android/app/src/main/AndroidManifest.xml:

<!-- این بلوک را حذف کنید -->
<queries>
<package android:name="ir.mservices.market" />
<intent>
<action android:name="ir.mservices.market.InAppBillingService.BIND" />
<data android:mimeType="*/*" />
</intent>
</queries>

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

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

تمام متدهای کلاس MyketIAP به‌صورت static بوده و یک Future برمی‌گردانند.

گام اول: ایمپورت پکیج و مدل‌ها​

import 'package:myket_iap/myket_iap.dart';
import 'package:myket_iap/util/iab_result.dart';
import 'package:myket_iap/util/purchase.dart';
import 'package:myket_iap/util/inventory.dart';
import 'package:myket_iap/util/sku_details.dart';
import 'package:myket_iap/util/constants.dart';

گام دوم: راه‌اندازی و اتصال سرویس (init)​

سرویس پرداخت را در ابتدای چرخه حیات برنامه (مثلاً در initState ویجت اصلی یا سرویس مدیریت پرداخت) مقداردهی کنید:


void initState() {
super.initState();
_initializeBilling();
}

Future<void> _initializeBilling() async {
const String myketRsaKey = "YOUR_RSA_KEY_FROM_MYKET_PANEL";

IabResult? result = await MyketIAP.init(
rsaKey: myketRsaKey,
enableDebugLogging: true, // در محیط Production مقدار false قرار دهید
);

if (result != null && result.isSuccess()) {
print("Billing setup successful: ${result.mMessage}");
// دریافت موجودی پس از اتصال موفق
_checkInventory();
} else {
print("Billing setup failed: ${result?.mMessage}");
}
}
هشدار

پیش از اطمینان از موفقیت اتصال init (result.isSuccess() == true)، از فراخوانی متدهای خرید یا استعلام موجودی خودداری کنید.

گام سوم: استعلام موجودی و مشخصات محصولات (queryInventory)​

برای استعلام خریدهای فعال کاربر و همچنین دریافت اطلاعات متا (عنوان، توضیحات و قیمت ریالی محصولات)، از متد queryInventory استفاده کنید:

Future<void> _checkInventory() async {
final Map<dynamic, dynamic> response = await MyketIAP.queryInventory(
querySkuDetails: true,
skus: ["gas", "premium_upgrade"],
);

IabResult? result = response[MyketIAP.RESULT];
Inventory? inventory = response[MyketIAP.INVENTORY];

if (result != null && result.isSuccess() && inventory != null) {
// ۱. بررسی مشخصات و قیمت محصولات
SkuDetails? gasDetails = inventory.mSkuMap["gas"];
if (gasDetails != null) {
print("Gas Price: ${gasDetails.mPrice}");
print("Gas Title: ${gasDetails.mTitle}");
}

// ۲. بررسی مالکیت محصولات غیرمصرفی
Purchase? premiumPurchase = inventory.mPurchaseMap["premium_upgrade"];
if (premiumPurchase != null) {
print("User owns premium upgrade: ${premiumPurchase.mOrderId}");
// فعال‌سازی دسترسی ویژه
}

// ۳. بازیابی و مصرف خریدهای مصرفی پردازش‌نشده (در صورت کرش یا قطعی قبل از مصرف)
Purchase? gasPurchase = inventory.mPurchaseMap["gas"];
if (gasPurchase != null) {
await _consumeProduct(gasPurchase);
}
} else {
print("Failed to query inventory: ${result?.mMessage}");
}
}

گام چهارم: شروع فرآیند خرید (launchPurchaseFlow)​

برای شروع فرآیند خرید یک محصول و نمایش درگاه پرداخت مایکت:

Future<void> _buyProduct(String sku) async {
// رشته اختیاری برای اعتبارسنجی سمت سرور و جلوگیری از تقلب
const String developerPayload = "custom_payload_or_order_token";

final Map<dynamic, dynamic> response = await MyketIAP.launchPurchaseFlow(
sku: sku,
payload: developerPayload,
);

IabResult? result = response[MyketIAP.RESULT];
Purchase? purchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess() && purchase != null) {
print("Purchase successful for SKU: ${purchase.mSku}");
print("Order ID: ${purchase.mOrderId}");
print("Purchase Token: ${purchase.mToken}");

// در صورت مصرفی بودن، بلافاصله مصرف شود
if (purchase.mSku == "gas") {
await _consumeProduct(purchase);
} else {
// محصول غیرمصرفی (ارتقای دائمی)
_unlockFeature();
}
} else {
print("Purchase cancelled or failed: ${result?.mMessage}");
}
}

گام پنجم: مصرف محصول خریداری‌شده (consume)​

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

Future<void> _consumeProduct(Purchase purchase) async {
final Map<dynamic, dynamic> response = await MyketIAP.consume(purchase: purchase);

IabResult? result = response[MyketIAP.RESULT];
Purchase? consumedPurchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess()) {
print("Item consumed successfully: ${consumedPurchase?.mSku}");
// اعطای کالا یا سکه به کاربر
} else {
print("Failed to consume item: ${result?.mMessage}");
}
}
اخطار مهم

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

گام ششم: استعلام وضعیت خرید یک محصول خاص (getPurchase)​

برای بررسی سریع اینکه آیا کاربر یک SKU خاص را قبلاً خریداری کرده است یا خیر:

Future<void> _checkPurchaseStatus(String sku) async {
final Map<dynamic, dynamic> response = await MyketIAP.getPurchase(
sku: sku,
querySkuDetails: true,
);

IabResult? result = response[MyketIAP.RESULT];
Purchase? purchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess() && purchase != null) {
print("SKU $sku is owned: ${purchase.mOrderId}");
} else {
print("SKU $sku is not owned or query failed.");
}
}

گام هفتم: آزادسازی منابع (dispose)​

در زمان بسته شدن ویجت یا اتمام کار با سرویس پرداخت، متد dispose را فراخوانی کنید:


void dispose() {
MyketIAP.dispose();
super.dispose();
}
نکته در مورد Future متد dispose

در نسخه 2.0.1، متد dispose اتصالات و منابع بومی اندروید را آزاد می‌کند اما پاسخی از طریق MethodChannel برنمی‌گرداند. بنابراین از await کردن نامحدود این متد در هنگام بستن برنامه خودداری کنید یا در صورت نیاز از .timeout(Duration(seconds: 2)) استفاده نمایید.

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

۱. بررسی Developer Payload:
همیشه یک شناسه یکتا یا توکن نشست کاربر در پارامتر payload ارسال کنید و پس از بازگشت نتیجه مطمئن شوید مقدار purchase.mDeveloperPayload با داده موردانتظار همخوانی دارد.

نکته

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

۲. اعتبارسنجی سمت سرور (Server-Side Validation):
برای تراکنش‌های حیاتی، مقادیر purchase.mOriginalJson و purchase.mSignature را به سرور امن خود بفرستید تا صحت امضا با کلید عمومی RSA و APIهای مایکت تأیید شود.
۳. عدم ذخیره کلید عمومی به‌صورت متن ساده:
از قرار دادن مستقیم و ناامن کلید RSA در مخازن عمومی گیت‌هاب خودداری کنید و از ابزارهای مدیریت کانفیگ یا انکود در زمان اجرا استفاده نمایید.
۴. جلوگیری از تحویل تکراری با توکن خرید:
مقدار purchase.mToken یک شناسه یکتا برای هر خرید است. این توکن را در پایگاه داده ثبت کنید تا در صورت ارسال مکرر درخواست، کالا به اشتباه دو بار به کاربر تحویل داده نشود.

مرجع API و مدل‌های داده​

کلاس MyketIAP​

متدپارامترهانوع خروجیتوضیحات
initrequired String rsaKey, bool enableDebugLogging = falseFuture<IabResult?>راه‌اندازی اولیه و برقراری ارتباط با سرویس پرداخت مایکت
launchPurchaseFlowrequired String sku, String? payloadFuture<Map>آغاز فرآیند خرید محصول و نمایش دیالوگ پرداخت مایکت
consumerequired Purchase purchaseFuture<Map>مصرف محصول مصرفی جهت امکان خرید دوباره آن
queryInventorybool querySkuDetails = false, List<String>? skusFuture<Map>استعلام تمام خریدهای فعال و مشخصات محصولات
getPurchaserequired String sku, bool querySkuDetails = falseFuture<Map>استعلام وضعیت خرید و اطلاعات مربوط به یک SKU خاص
disposeنداردFutureآزادسازی اتصالات سرویس و منابع حافظه بومی

کلاس IabResult​

نمایانگر وضعیت نتیجه یک عملیات پرداخت:

  • int mResponse: کد عددی پاسخ (کدهای خطا و پاسخ را در جدول زیر مشاهده کنید).
  • String mMessage: پیام متنی توصیف‌کننده نتیجه یا علت خطا.
  • bool isSuccess(): در صورت موفقیت‌آمیز بودن عملیات (BILLING_RESPONSE_RESULT_OK) مقدار true برمی‌گرداند.
  • bool isFailure(): در صورت بروز هرگونه خطا مقدار true برمی‌گرداند.

کلاس Purchase​

اطلاعات تراکنش و خرید انجام‌شده:

  • String mSku: شناسه محصول خریداری‌شده.
  • String mOrderId: شناسه یکتای سفارش تولیدشده توسط مایکت.
  • String mPackageName: نام پکیج برنامه خریدار.
  • int mPurchaseTime: زمان انجام تراکنش برحسب میلی‌ثانیه (Timestamp).
  • int mPurchaseState: وضعیت خرید (۰: موفق/خریداری‌شده، ۱: لغوشده، ۲: مرجوع‌شده).
  • String? mDeveloperPayload: داده تکمیلی ارسال‌شده در هنگام خرید.
  • String mToken: توکن یکتای خرید جهت اعتبارسنجی و مصرف محصول.
  • String mOriginalJson: داده خام JSON بازگردانده‌شده از سرویس پرداخت.
  • String mSignature: امضای رمزنگاری‌شده دیجیتال خرید.

کلاس SkuDetails​

مشخصات محصول ثبت‌شده در پنل توسعه‌دهندگان مایکت:

  • String mSku: شناسه یکتای محصول.
  • String mTitle: عنوان و نام محصول.
  • String mPrice: قیمت فرمت‌شده و محلی محصول (مانند ۱۰,۰۰۰ ریال).
  • String? mDescription: توضیحات محصول.
  • String mType: نوع محصول (inapp یا subs).

کلاس Inventory​

دربرگیرنده خریدهای فعال و مشخصات محصولات استعلام‌شده:

  • Map<String, Purchase> mPurchaseMap: نگاشت شناسه محصول به شیء Purchase.
  • Map<String, SkuDetails> mSkuMap: نگاشت شناسه محصول به شیء SkuDetails.

کدهای پاسخ و خطاهای پرداخت​

این مقادیر از طریق کلاس Constants در دسترس هستند:

کدهای پاسخ سرویس مایکت (Billing Response Codes)​

کدثابتتوضیحات
0BILLING_RESPONSE_RESULT_OKعملیات با موفقیت انجام شد.
1BILLING_RESPONSE_RESULT_USER_CANCELEDکاربر از خرید انصراف داد یا دیالوگ پرداخت را بست.
3BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLEسرویس پرداخت در دسترس نیست یا برنامه مایکت نصب نشده است.
4BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLEمحصول درخواستی در دسترس نیست یا در پنل فعال نشده است.
5BILLING_RESPONSE_RESULT_DEVELOPER_ERRORپارامترهای ارسالی نامعتبر یا خطای توسعه‌دهنده در فراخوانی API.
6BILLING_RESPONSE_RESULT_ERRORخطای عمومی و غیرمنتظره در حین فرآیند خرید.
7BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNEDکاربر پیش‌تر این محصول را خریده و مصرف نکرده است.
8BILLING_RESPONSE_RESULT_ITEM_NOT_OWNEDکاربر مالک این محصول نیست و امکان مصرف آن وجود ندارد.

کدهای خطای داخلی کتابخانه (IAB Helper Error Codes)​

کدثابتتوضیحات
-1001IABHELPER_REMOTE_EXCEPTIONخطای Remote Exception هنگام ارتباط با سرویس مایکت.
-1002IABHELPER_BAD_RESPONSEپاسخ نامعتبر یا ناقص از سرویس دریافت شده است.
-1003IABHELPER_VERIFICATION_FAILEDعدم تطابق و شکست در اعتبارسنجی امضای دیجیتال.
-1004IABHELPER_SEND_INTENT_FAILEDعدم امکان ارسال اینتنت پرداخت به مایکت.
-1005IABHELPER_USER_CANCELLEDلغو تراکنش توسط کاربر.
-1008IABHELPER_UNKNOWN_ERRORبروز خطای ناشناخته در لایه کلاینت.
-1010IABHELPER_INVALID_CONSUMPTIONتلاش برای مصرف محصولی که نامعتبر است یا در مالکیت کاربر نیست.

پروژه نمونه و تست اجرایی​

یک پروژه نمونه کاربردی و کامل به نام Trivial Drive شامل خرید بنزین مصرفی (gas) و عضویت ویژه غیرمصرفی (premium) در پوشه example در مخزن رسمی مایکت قرار دارد.

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

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

برای اجرای پروژه نمونه:

git clone https://github.com/myketstore/billing-sample-flutter.git
cd billing-sample-flutter/example
flutter pub get
flutter run

رفع اشکالات متداول (Troubleshooting)​

۱. خطای BILLING_UNAVAILABLE (کد ۳)​

  • مطمئن شوید برنامه بازارگاه مایکت روی دستگاه تست نصب و به‌روز است.
  • مطمئن شوید کاربر در حساب مایکت خود لاگین کرده است.

۲. خطای ITEM_UNAVAILABLE (کد ۴)​

  • مطمئن شوید شناسه SKU ارسالی در launchPurchaseFlow دقیقاً مطابق شناسه تعریف‌شده در پنل توسعه‌دهندگان مایکت است.
  • مطمئن شوید محصول در پنل توسعه‌دهندگان مایکت در وضعیت «فعال» قرار دارد.
  • بررسی کنید که applicationId در فایل build.gradle دقیقاً با پکیج ثبت‌شده در مایکت یکسان باشد.

۳. خطای VERIFICATION_FAILED (کد -1003)​

  • مطمئن شوید کلید عمومی RSA ارسال‌شده به MyketIAP.init() دقیقاً با کلید اختصاصی برنامه در پنل مایکت مطابقت دارد.