1 Commits
Author SHA1 Message Date
AmirmahdiandClaude Opus 4.8 2715bc720d feat: in-app terms & conditions + app name داروچی
- Add شرایط و قوانین page (full-screen dialog) reachable from drawer and login sheet
- TermsController + LegalContent + TermsDialog (store-agnostic wording)
- App bar title uses app_name; set app_name to داروچی

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:01:43 +03:30
13 changed files with 13 additions and 178 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+2 -18
View File
@@ -15,22 +15,10 @@ android {
applicationId = "com.approagency.pharmacy"
minSdk = 24
targetSdk = 36
versionCode = 2
versionName = "1.0.2+BZ"
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// ---- Bazaar (Cafe Bazaar) in-app billing ----
val marketApplicationId = "com.farsitel.bazaar"
val marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "com.farsitel.bazaar.permission.PAY_THROUGH_BAZAAR"
buildConfigField(
"String",
"IAB_PUBLIC_KEY",
"\"MIHNMA0GCSqGSIb3DQEBAQUAA4G7ADCBtwKBrwDzw+tRS/C//9JebsgTQOPAJU3Qwg7aIOhO4wqPVMNCjkGQZH7MckLU8hC6Rrag8gakkGBROSN/J58W4q1wxFbaUgRiTvyM04ElE7tHHa8CKtNH3NSUFzCQkhqd+9mLGflmj7JEj6EaZLoTLfZYhQgGDpWNXwW9HmO2lygXkua6BUkelpafgAmLz/ojlBm+ByLdSVkXIaaGqs2GLLR4qZHiksljQskbv0T0Ccax8pcCAwEAAQ==\""
)
}
buildTypes {
@@ -51,7 +39,6 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@@ -93,9 +80,6 @@ dependencies {
// SMS User Consent API (OTP auto-fill)
implementation(libs.play.services.auth.api.phone)
// Myket in-app billing
implementation("com.github.myketstore:myket-billing-client:1.6")
// Koin for DI
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose.v410)
+2 -2
View File
@@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 2,
"versionName": "1.0.2+MK",
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
-1
View File
@@ -2,7 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".DrugApp"
android:allowBackup="true"
@@ -1,135 +0,0 @@
package com.approagency.pharmacy.data.billing
import android.app.Activity
import android.content.Context
import com.approagency.pharmacy.BuildConfig
import com.approagency.pharmacy.data.local.SessionManager
import com.approagency.pharmacy.domain.billing.PurchaseGateway
import com.approagency.pharmacy.domain.billing.PurchaseResult
import com.approagency.pharmacy.domain.model.SubscriptionProduct
import ir.myket.billingclient.IabHelper
import ir.myket.billingclient.util.Purchase
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.UUID
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* درگاهِ پرداختِ کافه‌بازار بر پایهٔ همان کتابخانهٔ `myket-billing-client`
* (IabHelper). فروشگاهِ مقصد با `manifestPlaceholders` در build.gradle مشخص
* می‌شود (com.farsitel.bazaar)، بنابراین کدِ IabHelper یکسان است و فقط مقصدِ
* اتصال و کلیدِ عمومی فرق می‌کند.
*
* جریان:
* 1. اتصال به سرویس بازار ([ensureSetup]).
* 2. شروع خرید با SKU = [SubscriptionProduct.uuid] و دریافت توکنِ خرید.
* 3. توکن به سرور فرستاده می‌شود (با gateway = "bazaar") و پس از تأیید، خرید
* [consume] می‌شود تا کاربر بتواند دورهٔ بعد دوباره خرید کند.
*
* چون IabHelper در هر لحظه فقط یک عملیاتِ ناهمگام را می‌پذیرد، همهٔ فراخوانی‌ها
* با [mutex] ترتیبی می‌شوند.
*/
class BazaarPurchaseGateway(
context: Context,
private val session: SessionManager
) : PurchaseGateway {
// مقدارِ معتبرِ درگاه در سمتِ سرورِ آپرواجنسی برای کافه‌بازار «cafe» است.
override val name: String = "cafe"
override val isAvailable: Boolean = true
private val appContext = context.applicationContext
private val mutex = Mutex()
@Volatile private var helper: IabHelper? = null
@Volatile private var setupDone = false
/** خریدهای منتظرِ مصرف، برحسبِ توکن. */
private val pending = mutableMapOf<String, Purchase>()
override suspend fun purchase(
activity: Activity,
product: SubscriptionProduct
): Result<PurchaseResult> = mutex.withLock {
val sku = product.uuid?.takeIf { it.isNotBlank() }
?: return Result.failure(IllegalArgumentException("شناسهٔ محصول (SKU) نامعتبر است."))
ensureSetup().onFailure { return Result.failure(it) }
val helper = helper ?: return Result.failure(IllegalStateException("اتصال به بازار برقرار نشد."))
// developerPayload = شماره موبایلِ کاربر برای شناساییِ خریدار؛ در صورت نبود، توکن تصادفی.
val payload = session.account.value.mobile?.takeIf { it.isNotBlank() }
?: UUID.randomUUID().toString()
val outcome = runCatching {
suspendCancellableCoroutine { cont ->
// محصول‌های بازار از نوع «اشتراک» هستند.
helper.launchSubscriptionPurchaseFlow(
activity,
sku,
IabHelper.OnIabPurchaseFinishedListener { result, purchase ->
when {
result.isSuccess && purchase != null ->
cont.resume(PurchaseOutcome.Success(purchase))
result.response == IabHelper.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED ->
cont.resume(PurchaseOutcome.AlreadyOwned)
else ->
cont.resume(PurchaseOutcome.Error(result.message ?: "خطای پرداخت بازار"))
}
},
payload
)
}
}.getOrElse { return Result.failure(it) }
val purchase = when (outcome) {
is PurchaseOutcome.Success -> outcome.purchase
PurchaseOutcome.AlreadyOwned ->
return Result.failure(IllegalStateException("اشتراک شما از قبل فعال است."))
is PurchaseOutcome.Error -> return Result.failure(IllegalStateException(outcome.message))
}
val token = purchase.token
if (token.isNullOrBlank()) {
return Result.failure(IllegalStateException("توکن خرید دریافت نشد."))
}
pending[token] = purchase
Result.success(PurchaseResult(purchaseToken = token, orderId = purchase.orderId))
}
// اشتراکِ کافه‌بازار مصرف نمی‌شود؛ این متد بی‌اثر است.
override suspend fun consume(purchaseToken: String): Result<Unit> {
pending.remove(purchaseToken)
return Result.success(Unit)
}
/** اتصال به سرویس بازار (یک‌بار). */
private suspend fun ensureSetup(): Result<Unit> {
if (setupDone && helper != null) return Result.success(Unit)
val newHelper = IabHelper(appContext, BuildConfig.IAB_PUBLIC_KEY)
.apply { enableDebugLogging(BuildConfig.DEBUG) }
return runCatching {
suspendCancellableCoroutine { cont ->
newHelper.startSetup(IabHelper.OnIabSetupFinishedListener { result ->
if (result.isSuccess) cont.resume(Unit)
else cont.resumeWithException(
IllegalStateException(result.message ?: "اتصال به بازار ناموفق بود")
)
})
}
}.onSuccess {
helper = newHelper
setupDone = true
}.onFailure {
runCatching { newHelper.dispose() }
}
}
private sealed interface PurchaseOutcome {
data class Success(val purchase: Purchase) : PurchaseOutcome
data object AlreadyOwned : PurchaseOutcome
data class Error(val message: String) : PurchaseOutcome
}
}
@@ -22,6 +22,4 @@ class StubPurchaseGateway : PurchaseGateway {
product: SubscriptionProduct
): Result<PurchaseResult> =
Result.failure(IllegalStateException("درگاه پرداخت روی این نسخه فعال نیست."))
override suspend fun consume(purchaseToken: String): Result<Unit> = Result.success(Unit)
}
@@ -1,7 +1,7 @@
package com.approagency.pharmacy.di
import com.approagency.pharmacy.data.billing.BazaarPurchaseGateway
import com.approagency.pharmacy.data.billing.StubPurchaseGateway
import com.approagency.pharmacy.data.local.LocalDrugDataSource
import com.approagency.pharmacy.data.local.SessionManager
import com.approagency.pharmacy.data.local.database.LabDatabase
@@ -116,8 +116,8 @@ val appModule= module {
single<AuthRepository> { AuthRepositoryImpl(get(), get()) }
// درگاه پرداخت: روی شاخهٔ bazar پیاده‌سازی کافه‌بازار.
single<PurchaseGateway> { BazaarPurchaseGateway(androidContext(), get()) }
// درگاه پرداخت: روی main استاب؛ شاخه‌های myket/bazar جایگزین می‌کنند.
single<PurchaseGateway> { StubPurchaseGateway() }
factory { DrugHtmlParser() }
@@ -26,13 +26,6 @@ interface PurchaseGateway {
* نیاز به [activity] برای باز کردن جریان پرداخت فروشگاه دارد.
*/
suspend fun purchase(activity: Activity, product: SubscriptionProduct): Result<PurchaseResult>
/**
* مصرفِ خرید پس از تأییدِ سرور، تا کاربر بتواند دوباره (دورهٔ بعد) خرید کند.
* فقط برای درگاه‌هایی که مدلِ مصرف‌شدنی دارند (مثل مایکت) معنا دارد؛ برای
* بقیه می‌تواند بی‌اثر باشد.
*/
suspend fun consume(purchaseToken: String): Result<Unit>
}
/** نتیجه‌ی یک خرید موفق در فروشگاه. */
@@ -47,9 +47,9 @@ object AppLegal {
title = "۴. جست‌وجوی رایگان، اشتراک و پرداخت",
items = listOf(
"برنامه تعدادی جست‌وجوی رایگان در اختیار کاربر قرار می‌دهد؛ پس از پایانِ سهمیهٔ رایگان، برای استفادهٔ نامحدود تهیهٔ اشتراک لازم است.",
"خرید و پرداختِ اشتراک از طریق درگاهِ پرداختِ درون‌برنامه‌ایِ فروشگاه (کافه‌بازار) انجام می‌شود و تابعِ قوانینِ همان فروشگاه است.",
"خرید و پرداختِ اشتراک از طریق درگاهِ پرداختِ درون‌برنامه‌ایِ فروشگاه (فروشگاه) انجام می‌شود و تابعِ قوانینِ همان فروشگاه است.",
"قیمت و مدتِ اشتراک پیش از خرید به کاربر نمایش داده می‌شود.",
"بازگشتِ وجه تابعِ سیاست‌های فروشگاهِ کافه‌بازار است و از طریق همان فروشگاه پیگیری می‌شود.",
"بازگشتِ وجه تابعِ سیاست‌های فروشگاهِ فروشگاه است و از طریق همان فروشگاه پیگیری می‌شود.",
"در صورتی که پس از پرداختِ موفق اشتراک فعال نشد، با پشتیبانیِ ما تماس بگیرید."
)
),
@@ -135,8 +135,6 @@ class AccountViewModel(
}
repository.subscribe(product.id, result.purchaseToken, gateway.name).fold(
onSuccess = {
// پس از تأییدِ سرور، خرید را مصرف کن تا دورهٔ بعد قابل خرید باشد.
gateway.consume(result.purchaseToken)
_ui.update {
it.copy(
purchasingProductId = null,
+2 -2
View File
@@ -41,9 +41,9 @@
- برنامه تعدادی **جست‌وجوی رایگان** در اختیار کاربر قرار می‌دهد. پس از پایانِ
سهمیهٔ رایگان، برای ادامهٔ استفادهٔ نامحدود، تهیهٔ **اشتراک** لازم است.
- خرید و پرداختِ اشتراک از طریق **درگاهِ پرداختِ درون‌برنامه‌ایِ فروشگاه
(کافه‌بازار)** انجام می‌شود و تابعِ قوانین و شرایطِ همان فروشگاه است.
(فروشگاه)** انجام می‌شود و تابعِ قوانین و شرایطِ همان فروشگاه است.
- قیمت و مدتِ اشتراک پیش از خرید به کاربر نمایش داده می‌شود.
- **بازگشتِ وجه** تابعِ سیاست‌های فروشگاهِ کافه‌بازار است. درخواست‌های مربوط به
- **بازگشتِ وجه** تابعِ سیاست‌های فروشگاهِ فروشگاه است. درخواست‌های مربوط به
عودتِ وجهِ خریدهای درون‌برنامه‌ای از طریق همان فروشگاه پیگیری می‌شود.
- در صورتی که پس از پرداختِ موفق، اشتراک فعال نشد، با پشتیبانیِ ما تماس بگیرید.
-1
View File
@@ -18,7 +18,6 @@ dependencyResolutionManagement {
//google()
//mavenCentral()
maven { url = uri("https://maven.myket.ir") }
maven { url = uri("https://jitpack.io") }
}
}