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

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

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

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

کتابخانه iab-myket-reactnative ابزار رسمی برای پیاده‌سازی پرداخت درون‌برنامه‌ای مایکت در برنامه‌های React Native و Expo است. این کتابخانه از کامپوننت‌های تابعی (با هوک useMyket) و کامپوننت‌های مبتنی بر کلاس (با شیء myket) پشتیبانی می‌کند.

نکته: این کتابخانه فقط از سیستم‌عامل اندروید پشتیبانی می‌کند. در پروژه‌های Expo تهیه خروجی محلی بومی (Native Prebuild) الزامی است.

قابلیت‌ها​

  • رابط مبتنی بر Promise: پشتیبانی از ساختار async/await.
  • هوک React: ارائه هوک useMyket برای مدیریت چرخه اتصال در کامپوننت‌های تابعی.
  • پشتیبانی از Bare React Native و Expo: سازگار با پروژه‌های Bare و Managed Expo (از طریق Config Plugin).
  • پشتیبانی از انواع محصولات: مدیریت خریدهای یک‌باره شامل محصولات مصرف‌شدنی (سکه، اعتبار) و محصولات ماندگار و غیرمصرفی (ارتقا به نسخه ویژه، حذف تبلیغات).
  • استعلام موجودی و بازیابی خریدها: استعلام خریدهای فعال کاربر و بازیابی سفارش‌های مصرف‌نشده پس از قطعی ارتباط.
  • پشتیبانی از تایپ‌های TypeScript: همراه با تعاریف تایپ TypeScript.
  • لاگ‌های دیباگ بومی: امکان فعال‌سازی لاگ‌های Logcat با متد enableDebugging().

پیش‌نیازها​

پیش از شروع پیاده‌سازی، موارد زیر را آماده کنید:

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

تطابق شناسه بسته

مقدار applicationId در تنظیمات اندروید پروژه شما باید دقیقاً با شناسه بسته ثبت‌شده در پنل مایکت یکسان باشد.

نصب کتابخانه​

کتابخانه را با استفاده از ابزار مدیریت بسته دلخواه خود نصب کنید:

# با npm
npm install iab-myket-reactnative

# یا با yarn
yarn add iab-myket-reactnative
بیلد مجدد پروژه

پس از نصب کتابخانه، برنامه اندرویدی حتماً باید مجدداً کامپایل و بیلد شود (npx react-native run-android یا npx expo run:android). اجرای مجدد سرور Metro به‌تنهایی برای بارگذاری ماژول‌های بومی اندروید کافی نیست.

پیکربندی اندروید (Android Configuration)​

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

حالت اول: پروژه‌های استاندارد React Native (Bare)​

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

تنظیمات زیر را در بلوک android { defaultConfig { ... } } قرار دهید:

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

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

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

حالت دوم: پروژه‌های Expo (Managed / EAS Build)​

در پروژه‌های Expo که از Continuous Native Generation (CNG) یا دستور npx expo prebuild استفاده می‌کنند، از یک Config Plugin برای تزریق مشخصه‌ها به build.gradle استفاده می‌شود:

۱. ایجاد فایل plugins/withMarketPlace.js​

یک پوشه به نام plugins در ریشه پروژه ایجاد کرده و فایل withMarketPlace.js را در آن بسازید:

const { withAppBuildGradle } = require('@expo/config-plugins');

const withMarketPlace = (config, options = {}) => {
const { marketApplicationId, marketBindAddress, marketPermission } = options;

return withAppBuildGradle(config, (modConfig) => {
if (modConfig.modResults.language === 'groovy') {
const placeholders = `
manifestPlaceholders = [
marketApplicationId: "${marketApplicationId}",
marketBindAddress: "${marketBindAddress}",
marketPermission: "${marketPermission}"
]
`;
if (!modConfig.modResults.contents.includes('marketApplicationId')) {
modConfig.modResults.contents = modConfig.modResults.contents.replace(
/defaultConfig\s*\{/,
(match) => `${match}\n${placeholders}`
);
}
}
return modConfig;
});
};

module.exports = withMarketPlace;

۲. ثبت پلاگین در app.config.js​

const withMarketPlace = require('./plugins/withMarketPlace');

export default ({ config }) => ({
...config,
plugins: [
...(config.plugins || []),
[
withMarketPlace,
{
marketApplicationId: "ir.mservices.market",
marketBindAddress: "ir.mservices.market.InAppBillingService.BIND",
marketPermission: "ir.mservices.market.BILLING"
}
]
]
});

پس از اعمال تنظیمات، خروجی اندروید را تولید و اجرا کنید:

npx expo run:android

راهنمای پیاده‌سازی و نمونه‌کدها​

۱. پیاده‌سازی در کامپوننت‌های تابعی با هوک useMyket (توصیه‌شده)​

هوک useMyket اتصال خودکار به سرویس در زمان Mount شدن کامپوننت و قطع اتصال هنگام Unmount را مدیریت می‌کند:

import React, { useState } from 'react';
import { View, Button, Text, Alert } from 'react-native';
import { useMyket } from 'iab-myket-reactnative';

const RSA_PUBLIC_KEY = "YOUR_BASE64_RSA_PUBLIC_KEY_FROM_MYKET";

export default function ShopScreen() {
const myket = useMyket(RSA_PUBLIC_KEY);
const [loading, setLoading] = useState(false);

// استعلام محصولات و خریدهای مصرف‌نشده قبلی
const loadStoreItems = async () => {
try {
setLoading(true);
const inventory = await myket.queryPurchaseProduct(true, ['coin_pack_100', 'remove_ads']);
console.log('Available Products:', inventory.allProducts);

// مصرف خریدهای مصرفی پردازش‌نشده قبلی (بازیابی خرید)
for (const purchase of inventory.allPurchases) {
if (purchase.sku === 'coin_pack_100') {
await myket.consumePurchase(purchase);
console.log('Consumed pending purchase:', purchase.orderId);
}
}
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};

// شروع فرآیند خرید
const handleBuy = async (sku: string) => {
try {
const developerPayload = `payload_${Date.now()}`;
const purchase = await myket.purchaseProduct(sku, developerPayload);

// اعتبارسنجی payload
if (purchase.developerPayload === developerPayload) {
// اگر محصول از نوع مصرف‌شدنی باشد:
if (purchase.sku === 'coin_pack_100') {
await myket.consumePurchase(purchase);
Alert.alert('موفقیت', 'سکه با موفقیت خریداری و مصرف شد!');
} else {
// محصول غیرمصرفی (مانند حذف تبلیغات)
Alert.alert('موفقیت', 'ارتقا به نسخه ویژه با موفقیت انجام شد!');
}
}
} catch (error) {
Alert.alert('خطا در خرید', JSON.stringify(error));
}
};

return (
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Button title="دریافت موجودی و بازیابی" onPress={loadStoreItems} disabled={loading} />
<Button title="خرید بسته ۱۰۰ سکه" onPress={() => handleBuy('coin_pack_100')} />
</View>
);
}

۲. پیاده‌سازی در کامپوننت‌های کلاسی (شیء myket)​

در کامپوننت‌های کلاسی، چرخه حیات اتصال را با متدهای connect و disconnect مدیریت کنید:

import React, { Component } from 'react';
import { View, Button, Alert } from 'react-native';
import myket, { Inventory, Purchase } from 'iab-myket-reactnative';

const RSA_PUBLIC_KEY = "YOUR_BASE64_RSA_PUBLIC_KEY_FROM_MYKET";

class ShopScreen extends Component {
async componentDidMount() {
try {
// فعال‌سازی لاگ‌ها در محیط توسعه
myket.enableDebugging(__DEV__);

// اتصال به سرویس پرداخت مایکت
await myket.connect(RSA_PUBLIC_KEY);
this.fetchInventory();
} catch (error) {
console.error('Connection failed:', error);
}
}

componentWillUnmount() {
// آزادسازی منابع هنگام خروج از صفحه
myket.disconnect().catch(console.error);
}

fetchInventory = async () => {
try {
const inventory: Inventory = await myket.queryPurchaseProduct(true, ['premium_upgrade']);
console.log('Products:', inventory.allProducts);
} catch (error) {
console.error('Query failed:', error);
}
};

buyProduct = async (sku: string) => {
try {
const payload = 'my_unique_tracking_payload';
const purchase: Purchase = await myket.purchaseProduct(sku, payload);
Alert.alert('خرید موفق', `شناسه سفارش: ${purchase.orderId}`);
} catch (error) {
Alert.alert('خطا در خرید', JSON.stringify(error));
}
};

render() {
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Button title="ارتقا به نسخه ویژه" onPress={() => this.buyProduct('premium_upgrade')} />
</View>
);
}
}

export default ShopScreen;

مرجع API​

myket.connect(rsaKey: string): Promise<string>​

سرویس پرداخت مایکت را مقداردهی کرده و اتصال را برقرار می‌کند.

  • rsaKey: کلید عمومی RSA برنامه دریافت شده از پنل توسعه‌دهندگان مایکت.
  • خروجی: یک Promise که در صورت موفقیت با پیام متنی Resolve می‌شود یا در صورت بروز خطا (مانند عدم نصب مایکت) Reject می‌گردد.

myket.disconnect(): Promise<void>​

اتصال به سرویس پرداخت را قطع کرده و منابع بومی اندروید را آزاد می‌کند.

myket.enableDebugging(enable: boolean): void​

نمایش لاگ‌های دیباگ پرداخت را در Logcat اندروید فعال یا غیرفعال می‌کند.

myket.queryPurchaseProduct(querySkuDetails: boolean, moreSkus?: string[]): Promise<Inventory>​

اطلاعات محصولات و خریدهای فعال کاربر را استعلام می‌کند.

  • querySkuDetails: مقدار true برای دریافت مشخصات محصول (قیمت، عنوان، توضیحات)، و false برای دریافت صرف وضعیت خریدها.
  • moreSkus: آرایه‌ای از شناسه‌های SKU محصولات جهت استعلام قیمت و مشخصات.
  • خروجی: یک شیء Inventory شامل آرایه‌های allProducts و allPurchases.
بازیابی خریدهای مصرف‌نشده

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

myket.purchaseProduct(sku: string, developerPayload?: string): Promise<Purchase>​

دیالوگ پرداخت مایکت را برای یک محصول مشخص نمایش می‌دهد.

  • sku: شناسه یکتای محصول ثبت‌شده در پنل مایکت.
  • developerPayload: رشته اختیاری جهت اتصال به تراکنش برای اعتبارسنجی و جلوگیری از حملات Replay.
  • خروجی: در صورت پرداخت موفق، یک شیء Purchase برمی‌گرداند.

myket.consumePurchase(purchase: Purchase): Promise<void>​

محصول خریداری‌شده از نوع مصرف‌شدنی را مصرف می‌کند تا کاربر بتواند در آینده مجدداً آن را خریداری نماید.

  • purchase: شیء Purchase حاصل از خرید یا استعلام موجودی.
اخطار مهم

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

useMyket(rsaKey: string): typeof myket​

هوک اختصاصی React که با Mount شدن کامپوننت متصل شده و با Unmount شدن، اتصال را قطع می‌کند.

تعاریف TypeScript​

export interface Inventory {
allProducts: SkuDetails[];
allPurchases: Purchase[];
}

export interface SkuDetails {
itemType: string; // مانند "inapp"
sku: string; // شناسه محصول
type: string; // نوع محصول
price: string; // قیمت فرمت‌شده به همراه واحد پولی
title: string; // عنوان محصول
description: string; // توضیحات محصول
json: string; // داده خام JSON محصول
}

export interface Purchase {
itemType: string; // نوع محصول (مانند "inapp")
orderId: string; // شناسه یکتای سفارش مایکت
packageName: string; // نام پکیج برنامه خریدار
sku: string; // شناسه محصول
purchaseTime: string; // زمان خرید به میلی‌ثانیه (Timestamp)
purchaseState: number; // وضعیت خرید (۰: خریداری‌شده)
developerPayload: string; // اطلاعات تکمیلی ارسال‌شده در زمان خرید
token: string; // توکن یکتای خرید
originalJson: string; // داده خام JSON خرید
signature: string; // امضای دیجیتال رمزنگاری‌شده
}

مدیریت خطاها (Error Handling)​

در صورت بروز خطا در هر یک از متدها، Promise با یک خطا که حاوی پیام متنی یا ساختار زیر است Reject می‌شود:

{
"message": "Error description message",
"responseCode": -1008
}

خطاهای متداول و روش حل آن‌ها​

پیام خطاعلتراهکار
Myket application NotInstalled!برنامه بازارگاه مایکت روی دستگاه کاربر نصب نیست.کاربر را به نصب برنامه مایکت هدایت کنید.
Public key is null or empty!کلید عمومی RSA ارسال‌شده به connect خالی است.مطمئن شوید کلید عمومی معتبر Base64 از پنل مایکت ارسال شده است.
Another async operation in progress!یک عملیات خرید یا استعلام دیگر هنوز در حال اجرا است.منتظر بمانید تا عملیات قبلی با await تکمیل شود.
parameter 'sku' is null or empty!پارامتر SKU خالی ارسال شده است.شناسه معتبر محصول را ارسال کنید.
FetchError (responseCode)خطای بازگردانده‌شده از لایه بومی پرداخت مایکت.کد responseCode را برای تعیین دلیل دقیق بررسی کنید.

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

۱. بررسی Developer Payload:
همیشه یک رشته تصادفی و رمزنگاری‌شده (شامل شناسه کاربر و تراکنش) در developerPayload ارسال کنید و قبل از تحویل کالا مطمئن شوید با مقدار دریافتی در purchase.developerPayload مطابقت دارد.

نکته

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

۲. اعتبارسنجی سمت سرور با امضای دیجیتال:
برای تراکنش‌های مالی و محصولات حساس، مقادیر purchase.originalJson و purchase.signature را به سرور خود ارسال کرده و با استفاده از کلید عمومی RSA صحت تراکنش را در سرور بررسی کنید.
۳. ثبت توکن خرید جهت جلوگیری از تحویل تکراری:
مقدار purchase.token یک شناسه یکتا است. وضعیت هر توکن را در پایگاه داده ثبت کنید تا از تحویل مجدد اعتبار جلوگیری شود.
۴. مصرف سریع محصولات مصرفی:
بلافاصله پس از اعطای محصول به کاربر، متد consumePurchase را فراخوانی کنید. در صورت بروز هرگونه قطعی، در استعلام بعدی موجودی (queryPurchaseProduct) خریدهای مصرف‌نشده را مجدداً مصرف کنید.

پروژه نمونه​

یک برنامه نمونه کامل و کاربردی برای React Native و Expo در مخزن گیت‌هاب رسمی مایکت در دسترس است:

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

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