2 Commits
Author SHA1 Message Date
Amirmahdi 4a67bed0d3 feat: add terms and conditions 2026-06-14 21:47:22 +03:30
Amirmahdi cba6aa6153 feat: payment cafe 2026-06-14 21:17:51 +03:30
12 changed files with 402 additions and 57 deletions
+6 -6
View File
@@ -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==\""
) )
} }
-5
View File
@@ -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"
@@ -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
@@ -28,6 +28,7 @@ import com.approagency.pharmacy.domain.usecase.SearchDrugsYabUseCase
import com.approagency.pharmacy.domain.usecase.SearchTestsUseCase import com.approagency.pharmacy.domain.usecase.SearchTestsUseCase
import com.approagency.pharmacy.presentation.account.AccountSheetController import com.approagency.pharmacy.presentation.account.AccountSheetController
import com.approagency.pharmacy.presentation.account.OtpAutoFillBus import com.approagency.pharmacy.presentation.account.OtpAutoFillBus
import com.approagency.pharmacy.presentation.legal.TermsController
import com.approagency.pharmacy.presentation.viewModel.AccountViewModel import com.approagency.pharmacy.presentation.viewModel.AccountViewModel
import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel
import com.approagency.pharmacy.presentation.viewModel.HomeViewModel import com.approagency.pharmacy.presentation.viewModel.HomeViewModel
@@ -115,8 +116,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() }
@@ -191,6 +192,9 @@ val appModule= module {
// پلِ تحویل کدِ خوانده‌شده از پیامک به شیت ورود // پلِ تحویل کدِ خوانده‌شده از پیامک به شیت ورود
single { OtpAutoFillBus() } single { OtpAutoFillBus() }
// کنترلر نمایش صفحهٔ شرایط و قوانین
single { TermsController() }
viewModel { viewModel {
AccountViewModel(get(), get(), get()) AccountViewModel(get(), get(), get())
} }
@@ -11,7 +11,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import com.approagency.pharmacy.R
import com.approagency.pharmacy.data.local.AccountState import com.approagency.pharmacy.data.local.AccountState
/** /**
@@ -51,7 +53,7 @@ fun AccountAppBar(
) )
} }
} else { } else {
Text(text = "دارویاب", style = MaterialTheme.typography.titleMedium) Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.titleMedium)
} }
}, },
actions = { actions = {
@@ -23,6 +23,8 @@ import com.approagency.pharmacy.presentation.account.AccountDrawer
import com.approagency.pharmacy.presentation.account.AccountSheet import com.approagency.pharmacy.presentation.account.AccountSheet
import com.approagency.pharmacy.presentation.account.AccountSheetController import com.approagency.pharmacy.presentation.account.AccountSheetController
import com.approagency.pharmacy.presentation.common.ConfirmBottomSheet import com.approagency.pharmacy.presentation.common.ConfirmBottomSheet
import com.approagency.pharmacy.presentation.legal.TermsController
import com.approagency.pharmacy.presentation.legal.TermsDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -34,10 +36,12 @@ fun MainContainer(
) { ) {
val session: SessionManager = koinInject() val session: SessionManager = koinInject()
val sheetController: AccountSheetController = koinInject() val sheetController: AccountSheetController = koinInject()
val termsController: TermsController = koinInject()
val authRepository: AuthRepository = koinInject() val authRepository: AuthRepository = koinInject()
val account by session.account.collectAsState() val account by session.account.collectAsState()
val sheetVisible by sheetController.visible.collectAsState() val sheetVisible by sheetController.visible.collectAsState()
val termsVisible by termsController.visible.collectAsState()
val themeMode by session.themeMode.collectAsState() val themeMode by session.themeMode.collectAsState()
val fontScale by session.fontScale.collectAsState() val fontScale by session.fontScale.collectAsState()
@@ -62,6 +66,10 @@ fun MainContainer(
onThemeModeChange = { session.setThemeMode(it) }, onThemeModeChange = { session.setThemeMode(it) },
fontScale = fontScale, fontScale = fontScale,
onFontScaleChange = { session.setFontScale(it) }, onFontScaleChange = { session.setFontScale(it) },
onShowTerms = {
scope.launch { drawerState.close() }
termsController.show()
},
onLogout = { showLogoutConfirm = true } onLogout = { showLogoutConfirm = true }
) )
} }
@@ -86,6 +94,10 @@ fun MainContainer(
AccountSheet(onDismiss = { sheetController.hide() }) AccountSheet(onDismiss = { sheetController.hide() })
} }
if (termsVisible) {
TermsDialog(onDismiss = { termsController.hide() })
}
if (showLogoutConfirm) { if (showLogoutConfirm) {
ConfirmBottomSheet( ConfirmBottomSheet(
title = "خروج از حساب", title = "خروج از حساب",
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Article
import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.BrightnessAuto import androidx.compose.material.icons.filled.BrightnessAuto
import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.CalendarMonth
@@ -35,6 +36,7 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -65,6 +67,7 @@ fun AccountDrawer(
onThemeModeChange: (ThemeMode) -> Unit, onThemeModeChange: (ThemeMode) -> Unit,
fontScale: FontScale, fontScale: FontScale,
onFontScaleChange: (FontScale) -> Unit, onFontScaleChange: (FontScale) -> Unit,
onShowTerms: () -> Unit,
onLogout: () -> Unit onLogout: () -> Unit
) { ) {
ModalDrawerSheet( ModalDrawerSheet(
@@ -138,7 +141,26 @@ fun AccountDrawer(
Spacer(Modifier.height(MaterialTheme.dime.sm)) Spacer(Modifier.height(MaterialTheme.dime.sm))
FontScaleSelector(selected = fontScale, onSelect = onFontScaleChange) FontScaleSelector(selected = fontScale, onSelect = onFontScaleChange)
Spacer(Modifier.height(MaterialTheme.dime.xl)) Spacer(Modifier.height(MaterialTheme.dime.lg))
TextButton(
onClick = onShowTerms,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Article,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.width(MaterialTheme.dime.sm))
Text(
text = "شرایط و قوانین",
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
}
Spacer(Modifier.height(MaterialTheme.dime.md))
if (account.isLoggedIn) { if (account.isLoggedIn) {
OutlinedButton( OutlinedButton(
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet
import com.approagency.pharmacy.presentation.common.OtpTextField import com.approagency.pharmacy.presentation.common.OtpTextField
import com.approagency.pharmacy.presentation.common.PrimaryButton import com.approagency.pharmacy.presentation.common.PrimaryButton
import com.approagency.pharmacy.presentation.legal.TermsController
import com.approagency.pharmacy.presentation.viewModel.AccountPhase import com.approagency.pharmacy.presentation.viewModel.AccountPhase
import com.approagency.pharmacy.presentation.viewModel.AccountViewModel import com.approagency.pharmacy.presentation.viewModel.AccountViewModel
import com.vada.caller.ui.theme.dime import com.vada.caller.ui.theme.dime
@@ -51,6 +52,7 @@ fun AccountSheet(
val account by viewModel.account.collectAsState() val account by viewModel.account.collectAsState()
val activity = LocalContext.current as? Activity val activity = LocalContext.current as? Activity
val otpAutoFillBus: OtpAutoFillBus = koinInject() val otpAutoFillBus: OtpAutoFillBus = koinInject()
val termsController: TermsController = koinInject()
LaunchedEffect(Unit) { viewModel.onSheetOpened() } LaunchedEffect(Unit) { viewModel.onSheetOpened() }
@@ -109,6 +111,7 @@ fun AccountSheet(
} }
Spacer(Modifier.height(MaterialTheme.dime.md)) Spacer(Modifier.height(MaterialTheme.dime.md))
PrimaryButton(text = "ارسال کد", isLoading = ui.busy, onClick = { viewModel.sendOtp() }) PrimaryButton(text = "ارسال کد", isLoading = ui.busy, onClick = { viewModel.sendOtp() })
TermsConsentNote(onShowTerms = { termsController.show() })
} }
AccountPhase.EnterOtp -> { AccountPhase.EnterOtp -> {
@@ -199,3 +202,15 @@ private fun SheetTitle(text: String) {
Text(text = text, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) Text(text = text, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(MaterialTheme.dime.xs)) Spacer(Modifier.height(MaterialTheme.dime.xs))
} }
/** لینکِ «شرایط و قوانین» زیر فرمِ ورود (برای دسترسی و بازبینیِ فروشگاه). */
@Composable
private fun TermsConsentNote(onShowTerms: () -> Unit) {
TextButton(onClick = onShowTerms, modifier = Modifier.fillMaxWidth()) {
Text(
text = "با ادامه، شرایط و قوانین را می‌پذیرید",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
@@ -0,0 +1,100 @@
package com.approagency.pharmacy.presentation.legal
/** یک بخش از سند شرایط و قوانین. */
data class LegalSection(
val title: String,
val items: List<String>
)
/** محتوای شرایط و قوانینِ اپلیکیشن داروچی (برای نمایش درون‌برنامه‌ای). */
object AppLegal {
const val LAST_UPDATED = "۱۴۰۵/۰۳/۲۴"
const val SUPPORT_EMAIL = "approagency@gmail.com"
const val INTRO =
"استفاده از اپلیکیشن «داروچی» (که از این پس «برنامه» نامیده می‌شود) به‌منزلهٔ " +
"مطالعه و پذیرشِ کاملِ شرایط و قوانین زیر است. در صورتی که با هر بخشی از این " +
"قوانین موافق نیستید، لطفاً از برنامه استفاده نکنید."
val sections: List<LegalSection> = listOf(
LegalSection(
title = "۱. معرفی خدمات",
items = listOf(
"برنامه ابزاری اطلاع‌رسانی است که امکاناتِ جست‌وجوی دارو و مشاهدهٔ اطلاعات دارویی، مشاهدهٔ موجودی دارو در داروخانه‌ها و مشاهدهٔ اطلاعات آزمایش‌های پزشکی را فراهم می‌کند.",
"این خدمات صرفاً جنبهٔ آموزشی و اطلاع‌رسانی دارند."
)
),
LegalSection(
title = "۲. سلب مسئولیت پزشکی (بسیار مهم)",
items = listOf(
"اطلاعاتِ ارائه‌شده در برنامه جایگزینِ مشاورهٔ پزشک، داروساز یا کادر درمان نیست و نباید مبنای تشخیص، تجویز یا قطع/شروع مصرف دارو قرار گیرد.",
"مصرفِ هر دارو باید زیر نظر پزشک یا داروساز و مطابق نسخه و دستور ایشان انجام شود.",
"اطلاعاتِ دارویی ممکن است کامل، به‌روز یا بدون خطا نباشد و مسئولیتِ استفاده از آن بر عهدهٔ کاربر است.",
"موجودیِ نمایش‌داده‌شدهٔ داروها بر اساس داده‌های منابعِ ثالث است و پیش از مراجعه باید از داروخانه تأیید شود.",
"در شرایطِ اورژانسی یا بروزِ عوارض، فوراً به پزشک یا اورژانس مراجعه کنید."
)
),
LegalSection(
title = "۳. حساب کاربری و ورود",
items = listOf(
"ورود به برنامه از طریق شماره موبایل و کدِ یک‌بارمصرف (OTP) انجام می‌شود.",
"مسئولیتِ حفظِ شماره موبایل، دستگاه و کدِ تأیید بر عهدهٔ کاربر است.",
"کاربر موظف است اطلاعاتِ صحیح ارائه دهد و حساب را در اختیار دیگران قرار ندهد."
)
),
LegalSection(
title = "۴. جست‌وجوی رایگان، اشتراک و پرداخت",
items = listOf(
"برنامه تعدادی جست‌وجوی رایگان در اختیار کاربر قرار می‌دهد؛ پس از پایانِ سهمیهٔ رایگان، برای استفادهٔ نامحدود تهیهٔ اشتراک لازم است.",
"خرید و پرداختِ اشتراک از طریق درگاهِ پرداختِ درون‌برنامه‌ایِ فروشگاه (کافه‌بازار) انجام می‌شود و تابعِ قوانینِ همان فروشگاه است.",
"قیمت و مدتِ اشتراک پیش از خرید به کاربر نمایش داده می‌شود.",
"بازگشتِ وجه تابعِ سیاست‌های فروشگاهِ کافه‌بازار است و از طریق همان فروشگاه پیگیری می‌شود.",
"در صورتی که پس از پرداختِ موفق اشتراک فعال نشد، با پشتیبانیِ ما تماس بگیرید."
)
),
LegalSection(
title = "۵. حریم خصوصی و داده‌ها",
items = listOf(
"برای ارائهٔ خدمات، حداقلِ اطلاعاتِ لازم (از جمله شماره موبایل و وضعیتِ اشتراک) دریافت و نگه‌داری می‌شود.",
"اطلاعاتِ خرید (توکنِ خرید) صرفاً برای فعال‌سازیِ اشتراک به سرور ارسال می‌شود.",
"اطلاعاتِ کاربران بدون رضایتِ ایشان در اختیارِ اشخاصِ ثالثِ غیرمرتبط قرار نمی‌گیرد، مگر در مواردی که قانون الزام کند."
)
),
LegalSection(
title = "۶. تعهدات کاربر",
items = listOf(
"استفاده از برنامه صرفاً برای اهدافِ قانونی و شخصی.",
"ایجاد‌نکردنِ اخلال در عملکردِ برنامه و خودداری از مهندسیِ معکوس یا دورزدنِ محدودیت‌ها.",
"بازنشر یا تجاری‌سازیِ محتوای برنامه بدون اجازه ممنوع است."
)
),
LegalSection(
title = "۷. مالکیت فکری",
items = listOf(
"تمامِ حقوقِ مربوط به برنامه، نام، نشان، رابط کاربری و محتوای آن متعلق به مالکِ برنامه است و هرگونه استفادهٔ غیرمجاز پیگردِ قانونی دارد."
)
),
LegalSection(
title = "۸. محدودیت مسئولیت",
items = listOf(
"برنامه «همان‌گونه که هست» ارائه می‌شود و تضمینی نسبت به بی‌نقص بودن، در‌دسترس بودنِ دائمی یا صحتِ کاملِ اطلاعات داده نمی‌شود.",
"مالکِ برنامه در قبالِ خساراتِ مستقیم یا غیرمستقیمِ ناشی از استفاده یا اتکا به اطلاعاتِ برنامه مسئولیتی نمی‌پذیرد."
)
),
LegalSection(
title = "۹. تعلیق، تغییرات و قانونِ حاکم",
items = listOf(
"در صورتِ نقضِ این قوانین، دسترسیِ کاربر ممکن است محدود یا مسدود شود.",
"این شرایط ممکن است به‌روزرسانی شود و ادامهٔ استفاده به‌منزلهٔ پذیرشِ تغییرات است.",
"این شرایط تابعِ قوانینِ جمهوریِ اسلامیِ ایران است."
)
),
LegalSection(
title = "۱۰. ارتباط با ما",
items = listOf(
"در صورتِ هرگونه پرسش یا مشکل از طریقِ ایمیلِ $SUPPORT_EMAIL با ما در تماس باشید."
)
)
)
}
@@ -0,0 +1,17 @@
package com.approagency.pharmacy.presentation.legal
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* کنترلر سراسریِ نمایشِ صفحهٔ «شرایط و قوانین».
* هم کشوی کناری و هم شیتِ ورود میتوانند آن را با [show] باز کنند.
*/
class TermsController {
private val _visible = MutableStateFlow(false)
val visible: StateFlow<Boolean> = _visible.asStateFlow()
fun show() { _visible.value = true }
fun hide() { _visible.value = false }
}
@@ -0,0 +1,97 @@
package com.approagency.pharmacy.presentation.legal
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vada.caller.ui.theme.dime
/** صفحهٔ تمام‌صفحهٔ «شرایط و قوانین». */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TermsDialog(onDismiss: () -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
title = {
Text(
"شرایط و قوانین",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
},
navigationIcon = {
IconButton(onClick = onDismiss) {
Icon(Icons.Filled.Close, contentDescription = "بستن")
}
}
)
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(MaterialTheme.dime.lg),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
) {
Text(
text = "آخرین به‌روزرسانی: ${AppLegal.LAST_UPDATED}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = AppLegal.INTRO,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
AppLegal.sections.forEach { section ->
Spacer(Modifier.height(MaterialTheme.dime.xs))
Text(
text = section.title,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
section.items.forEach { item ->
Text(
text = "$item",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
Spacer(Modifier.height(MaterialTheme.dime.lg))
}
}
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
# شرایط و قوانین استفاده از اپلیکیشن داروچی
آخرین به‌روزرسانی: ۱۴۰۵/۰۳/۲۴
استفاده از اپلیکیشن «داروچی» (که از این پس «برنامه» نامیده می‌شود) به‌منزلهٔ
مطالعه و پذیرشِ کاملِ شرایط و قوانین زیر است. در صورتی که با هر بخشی از این
قوانین موافق نیستید، لطفاً از برنامه استفاده نکنید.
## ۱. معرفی خدمات
برنامه ابزاری اطلاع‌رسانی است که امکانات زیر را در اختیار کاربر قرار می‌دهد:
- جست‌وجوی دارو و مشاهدهٔ اطلاعات دارویی (موارد مصرف، مقدار مصرف، عوارض،
تداخلات، هشدارها و موارد مشابه).
- مشاهدهٔ موجودی دارو در داروخانه‌ها.
- مشاهدهٔ اطلاعات آزمایش‌ها و تست‌های پزشکی.
این خدمات صرفاً جنبهٔ آموزشی و اطلاع‌رسانی دارند.
## ۲. سلب مسئولیت پزشکی (بسیار مهم)
- اطلاعاتِ ارائه‌شده در برنامه **جایگزینِ مشاورهٔ پزشک، داروساز یا کادر درمان
نیست** و نباید مبنای تشخیص، تجویز یا قطع/شروع مصرف دارو قرار گیرد.
- مصرفِ هر دارو باید **زیر نظر پزشک یا داروساز** و مطابق نسخه و دستور ایشان
انجام شود.
- اطلاعاتِ دارویی ممکن است کامل، به‌روز یا بدون خطا نباشد. مسئولیتِ هرگونه
استفاده از این اطلاعات بر عهدهٔ کاربر است.
- موجودیِ نمایش‌داده‌شدهٔ داروها در داروخانه‌ها بر اساس داده‌های منابعِ ثالث
است و ممکن است با وضعیتِ واقعی متفاوت باشد؛ پیش از مراجعه، موجودی را از
داروخانه تأیید کنید.
- در شرایطِ اورژانسی یا بروزِ عوارض، فوراً به پزشک یا اورژانس مراجعه کنید.
## ۳. حساب کاربری و ورود
- ورود به برنامه از طریق شماره موبایل و کدِ یک‌بارمصرف (OTP) انجام می‌شود.
- مسئولیتِ حفظِ شماره موبایل، دسترسی به دستگاه و کدِ تأیید بر عهدهٔ کاربر است.
- کاربر موظف است اطلاعاتِ صحیح ارائه دهد و حساب را در اختیار دیگران قرار ندهد.
## ۴. جست‌وجوی رایگان، اشتراک و پرداخت
- برنامه تعدادی **جست‌وجوی رایگان** در اختیار کاربر قرار می‌دهد. پس از پایانِ
سهمیهٔ رایگان، برای ادامهٔ استفادهٔ نامحدود، تهیهٔ **اشتراک** لازم است.
- خرید و پرداختِ اشتراک از طریق **درگاهِ پرداختِ درون‌برنامه‌ایِ فروشگاه
(کافه‌بازار)** انجام می‌شود و تابعِ قوانین و شرایطِ همان فروشگاه است.
- قیمت و مدتِ اشتراک پیش از خرید به کاربر نمایش داده می‌شود.
- **بازگشتِ وجه** تابعِ سیاست‌های فروشگاهِ کافه‌بازار است. درخواست‌های مربوط به
عودتِ وجهِ خریدهای درون‌برنامه‌ای از طریق همان فروشگاه پیگیری می‌شود.
- در صورتی که پس از پرداختِ موفق، اشتراک فعال نشد، با پشتیبانیِ ما تماس بگیرید.
## ۵. حریم خصوصی و داده‌ها
- برای ارائهٔ خدمات، حداقلِ اطلاعاتِ لازم (از جمله **شماره موبایل** و وضعیتِ
اشتراک) دریافت و نگه‌داری می‌شود.
- اطلاعاتِ خرید (توکنِ خرید) صرفاً برای فعال‌سازیِ اشتراک به سرور ارسال می‌شود.
- اطلاعاتِ کاربران بدون رضایتِ ایشان در اختیارِ اشخاصِ ثالثِ غیرمرتبط قرار
نمی‌گیرد، مگر در مواردی که قانون الزام کند.
- برای آگاهیِ بیشتر، به سند **حریم خصوصی** برنامه مراجعه کنید.
## ۶. تعهدات کاربر
کاربر متعهد می‌شود که:
- از برنامه صرفاً برای اهدافِ قانونی و شخصی استفاده کند.
- در عملکردِ برنامه اخلال ایجاد نکند و از روش‌های نفوذ، مهندسیِ معکوس یا دور‌زدنِ
محدودیت‌ها استفاده نکند.
- محتوای برنامه را بدون اجازه بازنشر یا تجاری‌سازی نکند.
## ۷. مالکیت فکری
تمامِ حقوقِ مربوط به برنامه، نام، نشان (لوگو)، رابط کاربری و محتوای آن متعلق به
مالکِ برنامه است و هرگونه استفادهٔ غیرمجاز پیگردِ قانونی دارد.
## ۸. محدودیت مسئولیت
- برنامه «همان‌گونه که هست» ارائه می‌شود و هیچ تضمینی نسبت به بی‌نقص بودن،
در‌دسترس بودنِ دائمی یا صحتِ کاملِ اطلاعات داده نمی‌شود.
- مالکِ برنامه در قبالِ خساراتِ مستقیم یا غیرمستقیمِ ناشی از استفاده یا عدمِ
امکانِ استفاده از برنامه، یا اتکا به اطلاعاتِ آن، مسئولیتی نمی‌پذیرد.
## ۹. تعلیق و خاتمه
در صورتِ نقضِ این قوانین، دسترسیِ کاربر به برنامه ممکن است بدونِ اطلاعِ قبلی
محدود یا مسدود شود.
## ۱۰. تغییر قوانین
این شرایط ممکن است در آینده به‌روزرسانی شود. ادامهٔ استفاده از برنامه پس از
انتشارِ نسخهٔ جدید، به‌منزلهٔ پذیرشِ تغییرات است.
## ۱۱. قانونِ حاکم
این شرایط تابعِ قوانینِ جمهوریِ اسلامیِ ایران است و اختلافاتِ احتمالی مطابقِ همین
قوانین حل‌وفصل می‌شود.
## ۱۲. ارتباط با ما
در صورتِ هرگونه پرسش یا مشکل، از طریقِ راه‌های زیر با ما در تماس باشید:
- ایمیل پشتیبانی: approagency@gmail.com