feat: payment cafe
This commit is contained in:
@@ -16,20 +16,20 @@ android {
|
|||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 2
|
versionCode = 2
|
||||||
versionName = "1.0.2+MK"
|
versionName = "1.0.2+BZ"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// ---- Myket in-app billing ----
|
// ---- Bazaar (Cafe Bazaar) in-app billing ----
|
||||||
val marketApplicationId = "ir.mservices.market"
|
val marketApplicationId = "com.farsitel.bazaar"
|
||||||
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
|
val marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
|
||||||
manifestPlaceholders["marketApplicationId"] = marketApplicationId
|
manifestPlaceholders["marketApplicationId"] = marketApplicationId
|
||||||
manifestPlaceholders["marketBindAddress"] = marketBindAddress
|
manifestPlaceholders["marketBindAddress"] = marketBindAddress
|
||||||
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"
|
manifestPlaceholders["marketPermission"] = "com.farsitel.bazaar.permission.PAY_THROUGH_BAZAAR"
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"IAB_PUBLIC_KEY",
|
"IAB_PUBLIC_KEY",
|
||||||
"\"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDSaqJLDPxQZBZD/LGRsZOJ8m1hLVFedg/tnqebQusikVTNYSEjmP86i1TtXu/2b4E8hpkrqyNlNVvEg1lrHCpnxCh0tOubYiG6X/GEQtgOtIRuBzO3HM/r9w1HQm4gZOJc6DJO46Sxp3YbryJujp+pG8p7bU6zHp4zcqydCJ14QIDAQAB\""
|
"\"MIHNMA0GCSqGSIb3DQEBAQUAA4G7ADCBtwKBrwDzw+tRS/C//9JebsgTQOPAJU3Qwg7aIOhO4wqPVMNCjkGQZH7MckLU8hC6Rrag8gakkGBROSN/J58W4q1wxFbaUgRiTvyM04ElE7tHHa8CKtNH3NSUFzCQkhqd+9mLGflmj7JEj6EaZLoTLfZYhQgGDpWNXwW9HmO2lygXkua6BUkelpafgAmLz/ojlBm+ByLdSVkXIaaGqs2GLLR4qZHiksljQskbv0T0Ccax8pcCAwEAAQ==\""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<!-- دیدن و اتصال به سرویس پرداختِ مایکت روی اندروید ۱۱+ -->
|
|
||||||
<queries>
|
|
||||||
<package android:name="ir.mservices.market" />
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".DrugApp"
|
android:name=".DrugApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
+23
-41
@@ -17,23 +17,27 @@ import kotlin.coroutines.resume
|
|||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* درگاهِ پرداختِ مایکت بر پایهٔ کتابخانهٔ `myket-billing-client` (IabHelper).
|
* درگاهِ پرداختِ کافهبازار بر پایهٔ همان کتابخانهٔ `myket-billing-client`
|
||||||
|
* (IabHelper). فروشگاهِ مقصد با `manifestPlaceholders` در build.gradle مشخص
|
||||||
|
* میشود (com.farsitel.bazaar)، بنابراین کدِ IabHelper یکسان است و فقط مقصدِ
|
||||||
|
* اتصال و کلیدِ عمومی فرق میکند.
|
||||||
*
|
*
|
||||||
* جریان (مطابق همان مدلی که در اپ فلاترِ آپرواجنسی استفاده شده):
|
* جریان:
|
||||||
* 1. اتصال به سرویس مایکت ([ensureSetup]).
|
* 1. اتصال به سرویس بازار ([ensureSetup]).
|
||||||
* 2. شروع خرید با SKU = [SubscriptionProduct.uuid] و دریافت توکنِ خرید.
|
* 2. شروع خرید با SKU = [SubscriptionProduct.uuid] و دریافت توکنِ خرید.
|
||||||
* 3. توکن به سرور فرستاده میشود (در لایهٔ اشتراک) و پس از تأیید، خرید [consume]
|
* 3. توکن به سرور فرستاده میشود (با gateway = "bazaar") و پس از تأیید، خرید
|
||||||
* میشود تا کاربر بتواند دورهٔ بعد دوباره خرید کند.
|
* [consume] میشود تا کاربر بتواند دورهٔ بعد دوباره خرید کند.
|
||||||
*
|
*
|
||||||
* چون IabHelper در هر لحظه فقط یک عملیاتِ ناهمگام را میپذیرد، همهٔ فراخوانیها
|
* چون IabHelper در هر لحظه فقط یک عملیاتِ ناهمگام را میپذیرد، همهٔ فراخوانیها
|
||||||
* با [mutex] ترتیبی میشوند.
|
* با [mutex] ترتیبی میشوند.
|
||||||
*/
|
*/
|
||||||
class MyketPurchaseGateway(
|
class BazaarPurchaseGateway(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val session: SessionManager
|
private val session: SessionManager
|
||||||
) : PurchaseGateway {
|
) : PurchaseGateway {
|
||||||
|
|
||||||
override val name: String = "myket"
|
// مقدارِ معتبرِ درگاه در سمتِ سرورِ آپرواجنسی برای کافهبازار «cafe» است.
|
||||||
|
override val name: String = "cafe"
|
||||||
override val isAvailable: Boolean = true
|
override val isAvailable: Boolean = true
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
private val appContext = context.applicationContext
|
||||||
@@ -53,7 +57,7 @@ class MyketPurchaseGateway(
|
|||||||
?: return Result.failure(IllegalArgumentException("شناسهٔ محصول (SKU) نامعتبر است."))
|
?: return Result.failure(IllegalArgumentException("شناسهٔ محصول (SKU) نامعتبر است."))
|
||||||
|
|
||||||
ensureSetup().onFailure { return Result.failure(it) }
|
ensureSetup().onFailure { return Result.failure(it) }
|
||||||
val helper = helper ?: return Result.failure(IllegalStateException("اتصال به مایکت برقرار نشد."))
|
val helper = helper ?: return Result.failure(IllegalStateException("اتصال به بازار برقرار نشد."))
|
||||||
|
|
||||||
// developerPayload = شماره موبایلِ کاربر برای شناساییِ خریدار؛ در صورت نبود، توکن تصادفی.
|
// developerPayload = شماره موبایلِ کاربر برای شناساییِ خریدار؛ در صورت نبود، توکن تصادفی.
|
||||||
val payload = session.account.value.mobile?.takeIf { it.isNotBlank() }
|
val payload = session.account.value.mobile?.takeIf { it.isNotBlank() }
|
||||||
@@ -61,7 +65,8 @@ class MyketPurchaseGateway(
|
|||||||
|
|
||||||
val outcome = runCatching {
|
val outcome = runCatching {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
helper.launchPurchaseFlow(
|
// محصولهای بازار از نوع «اشتراک» هستند.
|
||||||
|
helper.launchSubscriptionPurchaseFlow(
|
||||||
activity,
|
activity,
|
||||||
sku,
|
sku,
|
||||||
IabHelper.OnIabPurchaseFinishedListener { result, purchase ->
|
IabHelper.OnIabPurchaseFinishedListener { result, purchase ->
|
||||||
@@ -71,7 +76,7 @@ class MyketPurchaseGateway(
|
|||||||
result.response == IabHelper.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED ->
|
result.response == IabHelper.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED ->
|
||||||
cont.resume(PurchaseOutcome.AlreadyOwned)
|
cont.resume(PurchaseOutcome.AlreadyOwned)
|
||||||
else ->
|
else ->
|
||||||
cont.resume(PurchaseOutcome.Error(result.message ?: "خطای پرداخت مایکت"))
|
cont.resume(PurchaseOutcome.Error(result.message ?: "خطای پرداخت بازار"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
payload
|
payload
|
||||||
@@ -81,8 +86,8 @@ class MyketPurchaseGateway(
|
|||||||
|
|
||||||
val purchase = when (outcome) {
|
val purchase = when (outcome) {
|
||||||
is PurchaseOutcome.Success -> outcome.purchase
|
is PurchaseOutcome.Success -> outcome.purchase
|
||||||
PurchaseOutcome.AlreadyOwned -> recoverOwned(sku)
|
PurchaseOutcome.AlreadyOwned ->
|
||||||
?: return Result.failure(IllegalStateException("این محصول قبلاً خریداری شده است."))
|
return Result.failure(IllegalStateException("اشتراک شما از قبل فعال است."))
|
||||||
is PurchaseOutcome.Error -> return Result.failure(IllegalStateException(outcome.message))
|
is PurchaseOutcome.Error -> return Result.failure(IllegalStateException(outcome.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,20 +99,13 @@ class MyketPurchaseGateway(
|
|||||||
Result.success(PurchaseResult(purchaseToken = token, orderId = purchase.orderId))
|
Result.success(PurchaseResult(purchaseToken = token, orderId = purchase.orderId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun consume(purchaseToken: String): Result<Unit> = mutex.withLock {
|
// اشتراکِ کافهبازار مصرف نمیشود؛ این متد بیاثر است.
|
||||||
val purchase = pending[purchaseToken] ?: return Result.success(Unit)
|
override suspend fun consume(purchaseToken: String): Result<Unit> {
|
||||||
val helper = helper ?: return Result.success(Unit)
|
pending.remove(purchaseToken)
|
||||||
runCatching {
|
return Result.success(Unit)
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
helper.consumeAsync(purchase, IabHelper.OnConsumeFinishedListener { _, result ->
|
|
||||||
if (result.isSuccess) cont.resume(Unit)
|
|
||||||
else cont.resumeWithException(IllegalStateException(result.message ?: "خطای مصرفِ خرید"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}.onSuccess { pending.remove(purchaseToken) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** اتصال به سرویس مایکت (یکبار). */
|
/** اتصال به سرویس بازار (یکبار). */
|
||||||
private suspend fun ensureSetup(): Result<Unit> {
|
private suspend fun ensureSetup(): Result<Unit> {
|
||||||
if (setupDone && helper != null) return Result.success(Unit)
|
if (setupDone && helper != null) return Result.success(Unit)
|
||||||
val newHelper = IabHelper(appContext, BuildConfig.IAB_PUBLIC_KEY)
|
val newHelper = IabHelper(appContext, BuildConfig.IAB_PUBLIC_KEY)
|
||||||
@@ -117,7 +115,7 @@ class MyketPurchaseGateway(
|
|||||||
newHelper.startSetup(IabHelper.OnIabSetupFinishedListener { result ->
|
newHelper.startSetup(IabHelper.OnIabSetupFinishedListener { result ->
|
||||||
if (result.isSuccess) cont.resume(Unit)
|
if (result.isSuccess) cont.resume(Unit)
|
||||||
else cont.resumeWithException(
|
else cont.resumeWithException(
|
||||||
IllegalStateException(result.message ?: "اتصال به مایکت ناموفق بود")
|
IllegalStateException(result.message ?: "اتصال به بازار ناموفق بود")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -129,22 +127,6 @@ class MyketPurchaseGateway(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** بازیابیِ خریدِ مصرفنشدهٔ موجود (وقتی محصول قبلاً خریده شده). */
|
|
||||||
private suspend fun recoverOwned(sku: String): Purchase? {
|
|
||||||
val helper = helper ?: return null
|
|
||||||
return runCatching {
|
|
||||||
suspendCancellableCoroutine<Purchase?> { cont ->
|
|
||||||
helper.queryInventoryAsync(
|
|
||||||
false,
|
|
||||||
listOf(sku),
|
|
||||||
IabHelper.QueryInventoryFinishedListener { result, inventory ->
|
|
||||||
cont.resume(if (result.isSuccess) inventory.getPurchase(sku) else null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface PurchaseOutcome {
|
private sealed interface PurchaseOutcome {
|
||||||
data class Success(val purchase: Purchase) : PurchaseOutcome
|
data class Success(val purchase: Purchase) : PurchaseOutcome
|
||||||
data object AlreadyOwned : PurchaseOutcome
|
data object AlreadyOwned : PurchaseOutcome
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.approagency.pharmacy.di
|
package com.approagency.pharmacy.di
|
||||||
|
|
||||||
|
|
||||||
import com.approagency.pharmacy.data.billing.MyketPurchaseGateway
|
import com.approagency.pharmacy.data.billing.BazaarPurchaseGateway
|
||||||
import com.approagency.pharmacy.data.local.LocalDrugDataSource
|
import com.approagency.pharmacy.data.local.LocalDrugDataSource
|
||||||
import com.approagency.pharmacy.data.local.SessionManager
|
import com.approagency.pharmacy.data.local.SessionManager
|
||||||
import com.approagency.pharmacy.data.local.database.LabDatabase
|
import com.approagency.pharmacy.data.local.database.LabDatabase
|
||||||
@@ -115,8 +115,8 @@ val appModule= module {
|
|||||||
|
|
||||||
single<AuthRepository> { AuthRepositoryImpl(get(), get()) }
|
single<AuthRepository> { AuthRepositoryImpl(get(), get()) }
|
||||||
|
|
||||||
// درگاه پرداخت: روی شاخهٔ myket پیادهسازی مایکت.
|
// درگاه پرداخت: روی شاخهٔ bazar پیادهسازی کافهبازار.
|
||||||
single<PurchaseGateway> { MyketPurchaseGateway(androidContext(), get()) }
|
single<PurchaseGateway> { BazaarPurchaseGateway(androidContext(), get()) }
|
||||||
|
|
||||||
|
|
||||||
factory { DrugHtmlParser() }
|
factory { DrugHtmlParser() }
|
||||||
|
|||||||
Reference in New Issue
Block a user