Compare commits
7
Commits
0a4b339481
...
myket
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79dfcc6916 | ||
|
|
f3b227f09a | ||
|
|
c76f8aa08f | ||
|
|
f266f4d142 | ||
|
|
1971708b3f | ||
|
|
27dc08fa68 | ||
|
|
1ed75fb823 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="21" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
+18
-2
@@ -15,10 +15,22 @@ android {
|
|||||||
applicationId = "com.approagency.pharmacy"
|
applicationId = "com.approagency.pharmacy"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "1.0.2+MK"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
// ---- Myket in-app billing ----
|
||||||
|
val marketApplicationId = "ir.mservices.market"
|
||||||
|
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
|
||||||
|
manifestPlaceholders["marketApplicationId"] = marketApplicationId
|
||||||
|
manifestPlaceholders["marketBindAddress"] = marketBindAddress
|
||||||
|
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"IAB_PUBLIC_KEY",
|
||||||
|
"\"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDSaqJLDPxQZBZD/LGRsZOJ8m1hLVFedg/tnqebQusikVTNYSEjmP86i1TtXu/2b4E8hpkrqyNlNVvEg1lrHCpnxCh0tOubYiG6X/GEQtgOtIRuBzO3HM/r9w1HQm4gZOJc6DJO46Sxp3YbryJujp+pG8p7bU6zHp4zcqydCJ14QIDAQAB\""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -39,6 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +93,9 @@ dependencies {
|
|||||||
// SMS User Consent API (OTP auto-fill)
|
// SMS User Consent API (OTP auto-fill)
|
||||||
implementation(libs.play.services.auth.api.phone)
|
implementation(libs.play.services.auth.api.phone)
|
||||||
|
|
||||||
|
// Myket in-app billing
|
||||||
|
implementation("com.github.myketstore:myket-billing-client:1.6")
|
||||||
|
|
||||||
// Koin for DI
|
// Koin for DI
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
implementation(libs.koin.androidx.compose.v410)
|
implementation(libs.koin.androidx.compose.v410)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 1,
|
"versionCode": 2,
|
||||||
"versionName": "1.0",
|
"versionName": "1.0.2+MK",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
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"
|
||||||
|
|||||||
Binary file not shown.
@@ -3,12 +3,18 @@ package com.approagency.pharmacy
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.approagency.pharmacy.data.local.database.LabDatabase
|
import com.approagency.pharmacy.data.local.database.LabDatabase
|
||||||
import com.approagency.pharmacy.di.appModule
|
import com.approagency.pharmacy.di.appModule
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
class DrugApp : Application(){
|
class DrugApp : Application() {
|
||||||
|
|
||||||
|
// اسکوپ سطحاپ برای کارهای پسزمینهی طولعمرِ برنامه (بهجای GlobalScope).
|
||||||
|
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startKoin {
|
startKoin {
|
||||||
@@ -17,8 +23,8 @@ class DrugApp : Application(){
|
|||||||
printLogger()
|
printLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preload database (optional, for faster access)
|
// پیشبارگذاری دیتابیس برای دسترسی سریعتر
|
||||||
GlobalScope.launch {
|
applicationScope.launch {
|
||||||
LabDatabase.getInstance(this@DrugApp)
|
LabDatabase.getInstance(this@DrugApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,18 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.approagency.pharmacy.data.local.SessionManager
|
import com.approagency.pharmacy.data.local.SessionManager
|
||||||
import com.approagency.pharmacy.domain.repository.AuthRepository
|
import com.approagency.pharmacy.domain.repository.AuthRepository
|
||||||
import com.approagency.pharmacy.navigation.AppNavGraph
|
import com.approagency.pharmacy.navigation.AppNavGraph
|
||||||
import com.approagency.pharmacy.presentation.account.OtpAutoFillBus
|
import com.approagency.pharmacy.presentation.account.OtpAutoFillBus
|
||||||
|
import com.approagency.pharmacy.presentation.account.OtpAutofillController
|
||||||
import com.approagency.pharmacy.ui.theme.DrugTheme
|
import com.approagency.pharmacy.ui.theme.DrugTheme
|
||||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||||
import com.google.android.gms.common.api.CommonStatusCodes
|
import com.google.android.gms.common.api.CommonStatusCodes
|
||||||
@@ -27,7 +31,7 @@ import com.google.android.gms.common.api.Status
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity(), OtpAutofillController {
|
||||||
|
|
||||||
private val session: SessionManager by inject()
|
private val session: SessionManager by inject()
|
||||||
private val authRepository: AuthRepository by inject()
|
private val authRepository: AuthRepository by inject()
|
||||||
@@ -51,8 +55,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
val themeMode by session.themeMode.collectAsState()
|
val themeMode by session.themeMode.collectAsState()
|
||||||
|
val fontScale by session.fontScale.collectAsState()
|
||||||
DrugTheme(themeMode = themeMode) {
|
DrugTheme(themeMode = themeMode) {
|
||||||
AppNavGraph()
|
// مقیاسِ متنِ انتخابیِ کاربر روی fontScaleِ سیستم اعمال میشود
|
||||||
|
// تا کلِ متنهای اپ یکجا بزرگ/کوچک شوند.
|
||||||
|
val density = LocalDensity.current
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(
|
||||||
|
density = density.density,
|
||||||
|
fontScale = density.fontScale * fontScale.scale
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppNavGraph()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +86,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// ---------- خودکارپُرکُنِ کد پیامک (SMS User Consent API) ----------
|
// ---------- خودکارپُرکُنِ کد پیامک (SMS User Consent API) ----------
|
||||||
|
|
||||||
/** آغاز گوشدادن به پیامکِ کد؛ کدِ یافتشده از طریق [OtpAutoFillBus] تحویل میشود. */
|
/** آغاز گوشدادن به پیامکِ کد؛ کدِ یافتشده از طریق [OtpAutoFillBus] تحویل میشود. */
|
||||||
fun startOtpAutofill() {
|
override fun startOtpAutofill() {
|
||||||
SmsRetriever.getClient(this).startSmsUserConsent(null)
|
SmsRetriever.getClient(this).startSmsUserConsent(null)
|
||||||
if (otpSmsReceiver != null) return
|
if (otpSmsReceiver != null) return
|
||||||
val receiver = object : BroadcastReceiver() {
|
val receiver = object : BroadcastReceiver() {
|
||||||
@@ -103,7 +118,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopOtpAutofill() {
|
override fun stopOtpAutofill() {
|
||||||
otpSmsReceiver?.let { runCatching { unregisterReceiver(it) } }
|
otpSmsReceiver?.let { runCatching { unregisterReceiver(it) } }
|
||||||
otpSmsReceiver = null
|
otpSmsReceiver = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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).
|
||||||
|
*
|
||||||
|
* جریان (مطابق همان مدلی که در اپ فلاترِ آپرواجنسی استفاده شده):
|
||||||
|
* 1. اتصال به سرویس مایکت ([ensureSetup]).
|
||||||
|
* 2. شروع خرید با SKU = [SubscriptionProduct.uuid] و دریافت توکنِ خرید.
|
||||||
|
* 3. توکن به سرور فرستاده میشود (در لایهٔ اشتراک) و پس از تأیید، خرید [consume]
|
||||||
|
* میشود تا کاربر بتواند دورهٔ بعد دوباره خرید کند.
|
||||||
|
*
|
||||||
|
* چون IabHelper در هر لحظه فقط یک عملیاتِ ناهمگام را میپذیرد، همهٔ فراخوانیها
|
||||||
|
* با [mutex] ترتیبی میشوند.
|
||||||
|
*/
|
||||||
|
class MyketPurchaseGateway(
|
||||||
|
context: Context,
|
||||||
|
private val session: SessionManager
|
||||||
|
) : PurchaseGateway {
|
||||||
|
|
||||||
|
override val name: String = "myket"
|
||||||
|
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.launchPurchaseFlow(
|
||||||
|
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 -> recoverOwned(sku)
|
||||||
|
?: 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> = mutex.withLock {
|
||||||
|
val purchase = pending[purchaseToken] ?: return Result.success(Unit)
|
||||||
|
val helper = helper ?: return Result.success(Unit)
|
||||||
|
runCatching {
|
||||||
|
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> {
|
||||||
|
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 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 {
|
||||||
|
data class Success(val purchase: Purchase) : PurchaseOutcome
|
||||||
|
data object AlreadyOwned : PurchaseOutcome
|
||||||
|
data class Error(val message: String) : PurchaseOutcome
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,4 +22,6 @@ class StubPurchaseGateway : PurchaseGateway {
|
|||||||
product: SubscriptionProduct
|
product: SubscriptionProduct
|
||||||
): Result<PurchaseResult> =
|
): Result<PurchaseResult> =
|
||||||
Result.failure(IllegalStateException("درگاه پرداخت روی این نسخه فعال نیست."))
|
Result.failure(IllegalStateException("درگاه پرداخت روی این نسخه فعال نیست."))
|
||||||
|
|
||||||
|
override suspend fun consume(purchaseToken: String): Result<Unit> = Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.approagency.pharmacy.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import com.approagency.pharmacy.data.dto.DarmanList
|
||||||
|
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugDetail
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugModels
|
||||||
|
import com.approagency.pharmacy.data.dto.GorohDaroei
|
||||||
|
import com.approagency.pharmacy.data.dto.GorohDarmani
|
||||||
|
import com.approagency.pharmacy.data.dto.Meta
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* منبعِ دادهٔ دارو روی پایگاهدادهٔ همراهِ اپ (`assets/DrugStore.db`).
|
||||||
|
*
|
||||||
|
* تنها منبعِ اطلاعاتِ دارو (جستوجو، جزئیات، گروههای درمانی) است و فقط خواندنی
|
||||||
|
* است. از SQLite خام استفاده میکند (نه Room) تا اعتبارسنجیِ سختگیرانهٔ شِمای
|
||||||
|
* Room روی این دیتابیسِ ثابتِ ازپیشساخته (با کلیدهای خارجی) دردسر نسازد.
|
||||||
|
*/
|
||||||
|
class LocalDrugDataSource(private val context: Context) {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var db: SQLiteDatabase? = null
|
||||||
|
|
||||||
|
private fun database(): SQLiteDatabase {
|
||||||
|
db?.let { return it }
|
||||||
|
return synchronized(this) {
|
||||||
|
db ?: openReadOnly().also { db = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openReadOnly(): SQLiteDatabase {
|
||||||
|
val out = context.getDatabasePath(DB_NAME)
|
||||||
|
if (!out.exists()) {
|
||||||
|
out.parentFile?.mkdirs()
|
||||||
|
context.assets.open(DB_NAME).use { input ->
|
||||||
|
FileOutputStream(out).use { output -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SQLiteDatabase.openDatabase(out.path, null, SQLiteDatabase.OPEN_READONLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جستوجوی دارو با فیلترهای نام، گروه دارویی و گروه درمانی. */
|
||||||
|
suspend fun searchDrugs(
|
||||||
|
query: String?,
|
||||||
|
drugGroup: Int?,
|
||||||
|
healGroup: Int?,
|
||||||
|
perPage: Int
|
||||||
|
): DrugListResponse = withContext(Dispatchers.IO) {
|
||||||
|
val where = StringBuilder("1=1")
|
||||||
|
val args = mutableListOf<String>()
|
||||||
|
if (!query.isNullOrBlank()) {
|
||||||
|
where.append(" AND (d.nam_fa LIKE ? OR d.nam_en LIKE ?)")
|
||||||
|
args.add("%$query%"); args.add("%$query%")
|
||||||
|
}
|
||||||
|
if (drugGroup != null) { where.append(" AND d.goroh_daroei_cod = ?"); args.add(drugGroup.toString()) }
|
||||||
|
if (healGroup != null) { where.append(" AND d.goroh_darmani_cod = ?"); args.add(healGroup.toString()) }
|
||||||
|
|
||||||
|
val total = database().rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM DrugInfo d WHERE $where", args.toTypedArray()
|
||||||
|
).use { c -> if (c.moveToFirst()) c.getInt(0) else 0 }
|
||||||
|
|
||||||
|
val items = database().rawQuery(
|
||||||
|
"$SELECT_WITH_RELATIONS WHERE $where ORDER BY d.nam_fa LIMIT ?",
|
||||||
|
(args + perPage.toString()).toTypedArray()
|
||||||
|
).use { c -> buildList { while (c.moveToNext()) add(c.toDrugDetail()) } }
|
||||||
|
|
||||||
|
DrugListResponse(
|
||||||
|
success = true,
|
||||||
|
message = null,
|
||||||
|
data = items,
|
||||||
|
meta = Meta(
|
||||||
|
current_page = 1,
|
||||||
|
last_page = if (perPage > 0) ceil(total.toDouble() / perPage).toInt() else 1,
|
||||||
|
per_page = perPage,
|
||||||
|
total = total
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جزئیات یک دارو بر اساس [cod]. اگر یافت نشود استثناء پرتاب میشود. */
|
||||||
|
suspend fun drugDetail(cod: Int): DrugModels = withContext(Dispatchers.IO) {
|
||||||
|
val detail = database().rawQuery(
|
||||||
|
"$SELECT_WITH_RELATIONS WHERE d.cod = ?", arrayOf(cod.toString())
|
||||||
|
).use { c -> if (c.moveToFirst()) c.toDrugDetail() else null }
|
||||||
|
?: throw NoSuchElementException("دارویی با شناسهٔ $cod در پایگاهدادهٔ محلی یافت نشد.")
|
||||||
|
|
||||||
|
DrugModels(success = true, message = null, data = detail, meta = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** فهرست گروههای درمانی (goroh_darmani). */
|
||||||
|
suspend fun darmanGroups(): DarmanModel = withContext(Dispatchers.IO) {
|
||||||
|
val groups = database().rawQuery(
|
||||||
|
"SELECT cod, nam_fa, nam_en FROM goroh_darmani ORDER BY nam_fa", null
|
||||||
|
).use { c ->
|
||||||
|
buildList {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
add(
|
||||||
|
DarmanList(
|
||||||
|
cod = c.getInt(0),
|
||||||
|
nam_fa = c.getStringOrEmpty(1),
|
||||||
|
nam_en = c.getStringOrEmpty(2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DarmanModel(success = "true", message = "", data = groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.toDrugDetail(): DrugDetail {
|
||||||
|
val daroeiCod = getIntOrNull("goroh_daroei_cod")
|
||||||
|
val darmaniCod = getIntOrNull("goroh_darmani_cod")
|
||||||
|
return DrugDetail(
|
||||||
|
cod = getIntOrNull("cod"),
|
||||||
|
goroh_darmani_detail_cod = getIntOrNull("goroh_darmani_detail_cod"),
|
||||||
|
goroh_daroei_cod = daroeiCod,
|
||||||
|
goroh_farmakologic_cod = getIntOrNull("goroh_farmakologic_cod"),
|
||||||
|
goroh_darmani_cod = darmaniCod,
|
||||||
|
nam_fa = getStringOrNull("nam_fa"),
|
||||||
|
nam_en = getStringOrNull("nam_en"),
|
||||||
|
mavaredmasraf = getStringOrNull("mavaredmasraf"),
|
||||||
|
meghdarmasraf = getStringOrNull("meghdarmasraf"),
|
||||||
|
masrafdarhamelegi = getStringOrNull("masrafdarhamelegi"),
|
||||||
|
masrafdarshirdehi = getStringOrNull("masrafdarshirdehi"),
|
||||||
|
manemasraf = getStringOrNull("manemasraf"),
|
||||||
|
avarez = getStringOrNull("avarez"),
|
||||||
|
tadakhol = getStringOrNull("tadakhol"),
|
||||||
|
mekanismtasir = getStringOrNull("mekanismtasir"),
|
||||||
|
nokte = getStringOrNull("nokte"),
|
||||||
|
hoshdar = getStringOrNull("hoshdar"),
|
||||||
|
sharayetnegahdari = getStringOrNull("sharayetnegahdari"),
|
||||||
|
ashkal_daroei = getStringOrNull("ashkal_daroei"),
|
||||||
|
created_at = null,
|
||||||
|
updated_at = null,
|
||||||
|
goroh_daroei = getStringOrNull("gd_nam")?.let { GorohDaroei(cod = daroeiCod, nam = it, created_at = null, updated_at = null) },
|
||||||
|
goroh_darmani = getStringOrNull("gm_nam_fa")?.let {
|
||||||
|
GorohDarmani(
|
||||||
|
cod = darmaniCod,
|
||||||
|
nam_fa = it,
|
||||||
|
nam_en = getStringOrNull("gm_nam_en"),
|
||||||
|
giyahi_ya_shimiyaei = getStringOrNull("gm_giyahi"),
|
||||||
|
image = getStringOrNull("gm_image"),
|
||||||
|
created_at = null,
|
||||||
|
updated_at = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
goroh_darmani_detail = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getStringOrNull(name: String): String? {
|
||||||
|
val i = getColumnIndex(name)
|
||||||
|
return if (i >= 0 && !isNull(i)) getString(i) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getStringOrEmpty(index: Int): String =
|
||||||
|
if (!isNull(index)) getString(index) else ""
|
||||||
|
|
||||||
|
private fun Cursor.getIntOrNull(name: String): Int? {
|
||||||
|
val i = getColumnIndex(name)
|
||||||
|
return if (i >= 0 && !isNull(i)) getInt(i) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DB_NAME = "DrugStore.db"
|
||||||
|
const val SELECT_WITH_RELATIONS = """
|
||||||
|
SELECT d.*,
|
||||||
|
gd.nam AS gd_nam,
|
||||||
|
gm.nam_fa AS gm_nam_fa,
|
||||||
|
gm.nam_en AS gm_nam_en,
|
||||||
|
gm.giyahi_ya_shimiyaei AS gm_giyahi,
|
||||||
|
gm.image AS gm_image
|
||||||
|
FROM DrugInfo d
|
||||||
|
LEFT JOIN goroh_daroei gd ON gd.cod = d.goroh_daroei_cod
|
||||||
|
LEFT JOIN goroh_darmani gm ON gm.cod = d.goroh_darmani_cod
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ package com.approagency.pharmacy.data.local
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.approagency.pharmacy.ui.theme.ThemeMode
|
import com.approagency.pharmacy.domain.model.FontScale
|
||||||
|
import com.approagency.pharmacy.domain.model.ThemeMode
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -128,6 +129,20 @@ class SessionManager(context: Context) {
|
|||||||
runCatching { ThemeMode.valueOf(prefs.getString(KEY_THEME_MODE, null) ?: ThemeMode.SYSTEM.name) }
|
runCatching { ThemeMode.valueOf(prefs.getString(KEY_THEME_MODE, null) ?: ThemeMode.SYSTEM.name) }
|
||||||
.getOrDefault(ThemeMode.SYSTEM)
|
.getOrDefault(ThemeMode.SYSTEM)
|
||||||
|
|
||||||
|
// ---------- اندازهٔ متن ----------
|
||||||
|
|
||||||
|
private val _fontScale = MutableStateFlow(readFontScale())
|
||||||
|
val fontScale: StateFlow<FontScale> = _fontScale.asStateFlow()
|
||||||
|
|
||||||
|
fun setFontScale(scale: FontScale) {
|
||||||
|
prefs.edit().putString(KEY_FONT_SCALE, scale.name).apply()
|
||||||
|
_fontScale.value = scale
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFontScale(): FontScale =
|
||||||
|
runCatching { FontScale.valueOf(prefs.getString(KEY_FONT_SCALE, null) ?: FontScale.NORMAL.name) }
|
||||||
|
.getOrDefault(FontScale.NORMAL)
|
||||||
|
|
||||||
private fun readAccount() = AccountState(
|
private fun readAccount() = AccountState(
|
||||||
isLoggedIn = !token.isNullOrBlank(),
|
isLoggedIn = !token.isNullOrBlank(),
|
||||||
mobile = prefs.getString(KEY_MOBILE, null),
|
mobile = prefs.getString(KEY_MOBILE, null),
|
||||||
@@ -147,5 +162,6 @@ class SessionManager(context: Context) {
|
|||||||
const val KEY_SUB_TITLE = "subscription_title"
|
const val KEY_SUB_TITLE = "subscription_title"
|
||||||
const val KEY_SUB_EXPIRE = "subscription_expire_at"
|
const val KEY_SUB_EXPIRE = "subscription_expire_at"
|
||||||
const val KEY_THEME_MODE = "theme_mode"
|
const val KEY_THEME_MODE = "theme_mode"
|
||||||
|
const val KEY_FONT_SCALE = "font_scale"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.approagency.pharmacy.data.remote
|
|
||||||
|
|
||||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
|
||||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
|
||||||
import com.approagency.pharmacy.data.dto.DrugModels
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface DrugApiService {
|
|
||||||
@GET("drugs/search")
|
|
||||||
suspend fun getDrugs(
|
|
||||||
@Query("q")query: String?,
|
|
||||||
@Query("per_page")perPage:Int? = 20,
|
|
||||||
@Query("with_relations")withRelations: Boolean? =true,
|
|
||||||
@Query("goroh_daroei_cod")drugGroup:Int?,
|
|
||||||
@Query("goroh_darmani_cod")healGroup:Int?,
|
|
||||||
): DrugListResponse
|
|
||||||
|
|
||||||
|
|
||||||
@GET("drugs/{cod}")
|
|
||||||
suspend fun getDrugDetail(
|
|
||||||
@Path("cod") cod: Int
|
|
||||||
): DrugModels
|
|
||||||
|
|
||||||
|
|
||||||
@GET("drugs/goroh-darmani")
|
|
||||||
suspend fun getGorohDaroei(): DarmanModel
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@ package com.approagency.pharmacy.data.repository
|
|||||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||||
import com.approagency.pharmacy.data.dto.DrugModels
|
import com.approagency.pharmacy.data.dto.DrugModels
|
||||||
|
import com.approagency.pharmacy.data.local.LocalDrugDataSource
|
||||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
||||||
import com.approagency.pharmacy.data.remote.DrugApiService
|
|
||||||
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
||||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||||
import com.approagency.pharmacy.data.remote.PharmacyHtmlParser
|
import com.approagency.pharmacy.data.remote.PharmacyHtmlParser
|
||||||
@@ -20,37 +20,39 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class DrugRepositoryImpl(
|
class DrugRepositoryImpl(
|
||||||
private val apiService: DrugApiService,
|
|
||||||
private val darooyabApiService: DarooyabApiService,
|
private val darooyabApiService: DarooyabApiService,
|
||||||
private val parser: DrugHtmlParser,
|
private val parser: DrugHtmlParser,
|
||||||
private val detailParser: DrugDetailParser,
|
private val detailParser: DrugDetailParser,
|
||||||
|
private val localDrugDataSource: LocalDrugDataSource,
|
||||||
): DrugRepository {
|
): DrugRepository {
|
||||||
|
// اطلاعات دارو همیشه از پایگاهدادهٔ محلیِ همراهِ اپ (assets/DrugStore.db) خوانده میشود.
|
||||||
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getDrugs(query = params.query , perPage = params.perPage , withRelations = params.withRelations , healGroup = params.healGroup , drugGroup = params.drugGroup )
|
Result.success(
|
||||||
return Result.success(response)
|
localDrugDataSource.searchDrugs(
|
||||||
}catch (e: Exception){
|
query = params.query,
|
||||||
|
drugGroup = params.drugGroup,
|
||||||
|
healGroup = params.healGroup,
|
||||||
|
perPage = params.perPage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
|
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
|
||||||
return try {
|
return try {
|
||||||
val response= apiService.getDrugDetail(cod = cod)
|
Result.success(localDrugDataSource.drugDetail(cod))
|
||||||
println(response.message)
|
} catch (e: Exception) {
|
||||||
return Result.success(response)
|
|
||||||
}catch (e: Exception){
|
|
||||||
println(e.message)
|
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getGorohDaroei(): Result<DarmanModel> {
|
override suspend fun getGorohDaroei(): Result<DarmanModel> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getGorohDaroei()
|
Result.success(localDrugDataSource.darmanGroups())
|
||||||
println(response)
|
} catch (e: Exception) {
|
||||||
return Result.success(response)
|
|
||||||
}catch (e: Exception){
|
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,8 +65,6 @@ class DrugRepositoryImpl(
|
|||||||
pageNumber = params.pageNumber
|
pageNumber = params.pageNumber
|
||||||
)
|
)
|
||||||
|
|
||||||
println("HTML Response length: ${htmlResponse.length}")
|
|
||||||
|
|
||||||
val searchResult = parser.parseSearchResultsWithPagination(htmlResponse)
|
val searchResult = parser.parseSearchResultsWithPagination(htmlResponse)
|
||||||
|
|
||||||
Result.success(searchResult)
|
Result.success(searchResult)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.approagency.pharmacy.di
|
package com.approagency.pharmacy.di
|
||||||
|
|
||||||
|
|
||||||
import com.approagency.pharmacy.data.billing.StubPurchaseGateway
|
import com.approagency.pharmacy.data.billing.MyketPurchaseGateway
|
||||||
|
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
|
||||||
import com.approagency.pharmacy.data.remote.ApproApiService
|
import com.approagency.pharmacy.data.remote.ApproApiService
|
||||||
import com.approagency.pharmacy.data.remote.AuthInterceptor
|
import com.approagency.pharmacy.data.remote.AuthInterceptor
|
||||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
||||||
import com.approagency.pharmacy.data.remote.DrugApiService
|
|
||||||
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
||||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||||
import com.approagency.pharmacy.data.repository.AuthRepositoryImpl
|
import com.approagency.pharmacy.data.repository.AuthRepositoryImpl
|
||||||
@@ -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
|
||||||
@@ -47,7 +48,6 @@ import retrofit2.converter.scalars.ScalarsConverterFactory
|
|||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.CookiePolicy
|
import java.net.CookiePolicy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
val jsonRetrofitQualifier = qualifier("jsonRetrofit")
|
|
||||||
val scalarRetrofitQualifier = qualifier("scalarRetrofit")
|
val scalarRetrofitQualifier = qualifier("scalarRetrofit")
|
||||||
val authRetrofitQualifier = qualifier("authRetrofit")
|
val authRetrofitQualifier = qualifier("authRetrofit")
|
||||||
val appModule= module {
|
val appModule= module {
|
||||||
@@ -76,19 +76,6 @@ val appModule= module {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
single<Retrofit>(jsonRetrofitQualifier) { // Add qualifier
|
|
||||||
Retrofit.Builder()
|
|
||||||
.baseUrl(Config.BASE_URL)
|
|
||||||
.client(get<OkHttpClient>())
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
single<DrugApiService> {
|
|
||||||
val retrofit: Retrofit = get(jsonRetrofitQualifier) // Get qualified instance
|
|
||||||
retrofit.create(DrugApiService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Retrofit for DAROOYAB Website ==========
|
// ========== Retrofit for DAROOYAB Website ==========
|
||||||
single<Retrofit>(scalarRetrofitQualifier) { // Add qualifier
|
single<Retrofit>(scalarRetrofitQualifier) { // Add qualifier
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
@@ -129,15 +116,18 @@ val appModule= module {
|
|||||||
|
|
||||||
single<AuthRepository> { AuthRepositoryImpl(get(), get()) }
|
single<AuthRepository> { AuthRepositoryImpl(get(), get()) }
|
||||||
|
|
||||||
// درگاه پرداخت: روی main استاب؛ شاخههای myket/bazar جایگزین میکنند.
|
// درگاه پرداخت: روی شاخهٔ myket پیادهسازی مایکت.
|
||||||
single<PurchaseGateway> { StubPurchaseGateway() }
|
single<PurchaseGateway> { MyketPurchaseGateway(androidContext(), get()) }
|
||||||
|
|
||||||
|
|
||||||
factory { DrugHtmlParser() }
|
factory { DrugHtmlParser() }
|
||||||
factory { DrugDetailParser() }
|
factory { DrugDetailParser() }
|
||||||
|
// منبع آفلاین دارو (assets/DrugStore.db) برای fallback هنگام خطای شبکه
|
||||||
|
single { LocalDrugDataSource(androidContext()) }
|
||||||
|
|
||||||
//repo
|
//repo
|
||||||
single<DrugRepository> {
|
single<DrugRepository> {
|
||||||
DrugRepositoryImpl(get<DrugApiService>() , get<DarooyabApiService>() , get() , get())
|
DrugRepositoryImpl(get<DarooyabApiService>() , get() , get() , get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -202,6 +192,9 @@ val appModule= module {
|
|||||||
// پلِ تحویل کدِ خواندهشده از پیامک به شیت ورود
|
// پلِ تحویل کدِ خواندهشده از پیامک به شیت ورود
|
||||||
single { OtpAutoFillBus() }
|
single { OtpAutoFillBus() }
|
||||||
|
|
||||||
|
// کنترلر نمایش صفحهٔ شرایط و قوانین
|
||||||
|
single { TermsController() }
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
AccountViewModel(get(), get(), get())
|
AccountViewModel(get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ interface PurchaseGateway {
|
|||||||
* نیاز به [activity] برای باز کردن جریان پرداخت فروشگاه دارد.
|
* نیاز به [activity] برای باز کردن جریان پرداخت فروشگاه دارد.
|
||||||
*/
|
*/
|
||||||
suspend fun purchase(activity: Activity, product: SubscriptionProduct): Result<PurchaseResult>
|
suspend fun purchase(activity: Activity, product: SubscriptionProduct): Result<PurchaseResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* مصرفِ خرید پس از تأییدِ سرور، تا کاربر بتواند دوباره (دورهٔ بعد) خرید کند.
|
||||||
|
* فقط برای درگاههایی که مدلِ مصرفشدنی دارند (مثل مایکت) معنا دارد؛ برای
|
||||||
|
* بقیه میتواند بیاثر باشد.
|
||||||
|
*/
|
||||||
|
suspend fun consume(purchaseToken: String): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** نتیجهی یک خرید موفق در فروشگاه. */
|
/** نتیجهی یک خرید موفق در فروشگاه. */
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.approagency.pharmacy.domain.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* اندازهٔ متنِ انتخابیِ کاربر (بهویژه برای کاربرانِ مسنتر).
|
||||||
|
* [scale] ضریبی است که روی fontScaleِ دستگاه اعمال میشود.
|
||||||
|
*/
|
||||||
|
enum class FontScale(val scale: Float, val label: String) {
|
||||||
|
NORMAL(1.0f, "معمولی"),
|
||||||
|
LARGE(1.2f, "بزرگ"),
|
||||||
|
XLARGE(1.45f, "خیلی بزرگ")
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.approagency.pharmacy.ui.theme
|
package com.approagency.pharmacy.domain.model
|
||||||
|
|
||||||
/** حالت نمایش انتخابی کاربر؛ پیشفرض پیروی از سیستم است. */
|
/** حالت نمایش انتخابی کاربر؛ پیشفرض پیروی از سیستم است. */
|
||||||
enum class ThemeMode { SYSTEM, LIGHT, DARK }
|
enum class ThemeMode { SYSTEM, LIGHT, DARK }
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.approagency.pharmacy.navigation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import com.approagency.pharmacy.R
|
||||||
|
import com.approagency.pharmacy.data.local.AccountState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* نوار بالای اپ: دکمهی منو، اطلاعات حساب (موبایل + وضعیت اشتراک) و کنشِ
|
||||||
|
* ورود/خرید اشتراک.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountAppBar(
|
||||||
|
account: AccountState,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
onAccountClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onMenuClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Menu,
|
||||||
|
contentDescription = "منو"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
if (account.isLoggedIn) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = account.mobile ?: account.displayName.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = account.subscriptionTitle ?: "بدون اشتراک",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = if (account.isSubscribed)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (!account.isLoggedIn) {
|
||||||
|
ElevatedButton(onClick = onAccountClick) {
|
||||||
|
Text("ورود", textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
} else if (!account.isSubscribed) {
|
||||||
|
ElevatedButton(onClick = onAccountClick) {
|
||||||
|
Text("خرید اشتراک", textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ fun AppNavGraph() {
|
|||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "home"
|
startDestination = "search"
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// ───────────────
|
// ───────────────
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ fun BottomBar(navController: NavHostController) {
|
|||||||
.clickable {
|
.clickable {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
navController.navigate(item.first) {
|
navController.navigate(item.first) {
|
||||||
popUpTo("home") {
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
saveState = true
|
saveState = true
|
||||||
}
|
}
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
package com.approagency.pharmacy.navigation
|
package com.approagency.pharmacy.navigation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.Menu
|
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.material3.ElevatedButton
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -27,15 +16,15 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.approagency.pharmacy.data.local.AccountState
|
|
||||||
import com.approagency.pharmacy.data.local.SessionManager
|
import com.approagency.pharmacy.data.local.SessionManager
|
||||||
import com.approagency.pharmacy.domain.repository.AuthRepository
|
import com.approagency.pharmacy.domain.repository.AuthRepository
|
||||||
import com.approagency.pharmacy.presentation.account.AccountDrawer
|
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
|
||||||
|
|
||||||
@@ -47,11 +36,14 @@ 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 drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -72,6 +64,12 @@ fun MainContainer(
|
|||||||
appVersion = appVersion,
|
appVersion = appVersion,
|
||||||
themeMode = themeMode,
|
themeMode = themeMode,
|
||||||
onThemeModeChange = { session.setThemeMode(it) },
|
onThemeModeChange = { session.setThemeMode(it) },
|
||||||
|
fontScale = fontScale,
|
||||||
|
onFontScaleChange = { session.setFontScale(it) },
|
||||||
|
onShowTerms = {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
termsController.show()
|
||||||
|
},
|
||||||
onLogout = { showLogoutConfirm = true }
|
onLogout = { showLogoutConfirm = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,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 = "خروج از حساب",
|
||||||
@@ -112,53 +114,3 @@ fun MainContainer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun AccountAppBar(
|
|
||||||
account: AccountState,
|
|
||||||
onMenuClick: () -> Unit,
|
|
||||||
onAccountClick: () -> Unit
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onMenuClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Menu,
|
|
||||||
contentDescription = "منو"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
if (account.isLoggedIn) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = account.mobile ?: account.displayName.orEmpty(),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = account.subscriptionTitle ?: "بدون اشتراک",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = if (account.isSubscribed)
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(text = "دارویاب", style = MaterialTheme.typography.titleMedium)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (!account.isLoggedIn) {
|
|
||||||
ElevatedButton(onClick = onAccountClick) {
|
|
||||||
Text("ورود", textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
}else if (!account.isSubscribed){
|
|
||||||
ElevatedButton(onClick = onAccountClick) {
|
|
||||||
Text("خرید اشتراک", textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.approagency.pharmacy.presentation.account
|
package com.approagency.pharmacy.presentation.account
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -14,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
|
||||||
@@ -33,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
|
||||||
@@ -43,7 +47,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.approagency.pharmacy.data.local.AccountState
|
import com.approagency.pharmacy.data.local.AccountState
|
||||||
import com.approagency.pharmacy.ui.theme.ThemeMode
|
import com.approagency.pharmacy.domain.model.FontScale
|
||||||
|
import com.approagency.pharmacy.domain.model.ThemeMode
|
||||||
import com.vada.caller.ui.theme.dime
|
import com.vada.caller.ui.theme.dime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +65,9 @@ fun AccountDrawer(
|
|||||||
appVersion: String,
|
appVersion: String,
|
||||||
themeMode: ThemeMode,
|
themeMode: ThemeMode,
|
||||||
onThemeModeChange: (ThemeMode) -> Unit,
|
onThemeModeChange: (ThemeMode) -> Unit,
|
||||||
|
fontScale: FontScale,
|
||||||
|
onFontScaleChange: (FontScale) -> Unit,
|
||||||
|
onShowTerms: () -> Unit,
|
||||||
onLogout: () -> Unit
|
onLogout: () -> Unit
|
||||||
) {
|
) {
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
@@ -71,6 +79,7 @@ fun AccountDrawer(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = MaterialTheme.dime.lg, vertical = MaterialTheme.dime.xl)
|
.padding(horizontal = MaterialTheme.dime.lg, vertical = MaterialTheme.dime.xl)
|
||||||
) {
|
) {
|
||||||
Header(account)
|
Header(account)
|
||||||
@@ -121,7 +130,37 @@ fun AccountDrawer(
|
|||||||
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
ThemeModeSelector(selected = themeMode, onSelect = onThemeModeChange)
|
ThemeModeSelector(selected = themeMode, onSelect = onThemeModeChange)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.height(MaterialTheme.dime.lg))
|
||||||
|
|
||||||
|
// اندازهٔ متن (برای کاربرانِ مسنتر)
|
||||||
|
Text(
|
||||||
|
text = "اندازهٔ متن",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
|
FontScaleSelector(selected = fontScale, onSelect = onFontScaleChange)
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -267,3 +306,23 @@ private fun ThemeModeSelector(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FontScaleSelector(
|
||||||
|
selected: FontScale,
|
||||||
|
onSelect: (FontScale) -> Unit
|
||||||
|
) {
|
||||||
|
val options = FontScale.entries
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
options.forEachIndexed { index, option ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = selected == option,
|
||||||
|
onClick = { onSelect(option) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||||
|
icon = {},
|
||||||
|
label = { Text(option.label, style = MaterialTheme.typography.labelSmall) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -26,7 +24,6 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
@@ -35,12 +32,10 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.approagency.pharmacy.MainActivity
|
|
||||||
import com.approagency.pharmacy.domain.model.SubscriptionProduct
|
|
||||||
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.common.shimmer
|
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
|
||||||
@@ -57,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() }
|
||||||
|
|
||||||
@@ -71,9 +67,9 @@ fun AccountSheet(
|
|||||||
// در مرحلهی کد، گوشدادن به پیامک را آغاز کن و با خروج متوقفش کن.
|
// در مرحلهی کد، گوشدادن به پیامک را آغاز کن و با خروج متوقفش کن.
|
||||||
val isOtpStep = ui.phase == AccountPhase.EnterOtp
|
val isOtpStep = ui.phase == AccountPhase.EnterOtp
|
||||||
DisposableEffect(isOtpStep) {
|
DisposableEffect(isOtpStep) {
|
||||||
val mainActivity = activity as? MainActivity
|
val autofill = activity as? OtpAutofillController
|
||||||
if (isOtpStep) mainActivity?.startOtpAutofill()
|
if (isOtpStep) autofill?.startOtpAutofill()
|
||||||
onDispose { mainActivity?.stopOtpAutofill() }
|
onDispose { autofill?.stopOtpAutofill() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// کدِ خواندهشده از پیامک را در فیلد بگذار و بهصورت خودکار تأیید کن.
|
// کدِ خواندهشده از پیامک را در فیلد بگذار و بهصورت خودکار تأیید کن.
|
||||||
@@ -115,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 -> {
|
||||||
@@ -151,13 +148,13 @@ fun AccountSheet(
|
|||||||
ui.productsLoading -> Column(
|
ui.productsLoading -> Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
|
||||||
) {
|
) {
|
||||||
repeat(2) { ProductCardSkeleton() }
|
repeat(2) { SubscriptionProductCardSkeleton() }
|
||||||
}
|
}
|
||||||
else -> Column(
|
else -> Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
|
||||||
) {
|
) {
|
||||||
ui.products.forEach { product ->
|
ui.products.forEach { product ->
|
||||||
ProductCard(
|
SubscriptionProductCard(
|
||||||
product = product,
|
product = product,
|
||||||
isPurchasing = ui.purchasingProductId == product.id,
|
isPurchasing = ui.purchasingProductId == product.id,
|
||||||
onBuy = { activity?.let { viewModel.purchase(it, product) } }
|
onBuy = { activity?.let { viewModel.purchase(it, product) } }
|
||||||
@@ -206,73 +203,14 @@ private fun SheetTitle(text: String) {
|
|||||||
Spacer(Modifier.height(MaterialTheme.dime.xs))
|
Spacer(Modifier.height(MaterialTheme.dime.xs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** لینکِ «شرایط و قوانین» زیر فرمِ ورود (برای دسترسی و بازبینیِ فروشگاه). */
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProductCard(
|
private fun TermsConsentNote(onShowTerms: () -> Unit) {
|
||||||
product: SubscriptionProduct,
|
TextButton(onClick = onShowTerms, modifier = Modifier.fillMaxWidth()) {
|
||||||
isPurchasing: Boolean,
|
Text(
|
||||||
onBuy: () -> Unit
|
text = "با ادامه، شرایط و قوانین را میپذیرید",
|
||||||
) {
|
style = MaterialTheme.typography.labelMedium,
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
color = MaterialTheme.colorScheme.primary
|
||||||
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
|
)
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(product.title, style = MaterialTheme.typography.titleMedium)
|
|
||||||
Text(
|
|
||||||
"${formatPrice(product.price)} تومان",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!product.description.isNullOrBlank()) {
|
|
||||||
Spacer(Modifier.height(MaterialTheme.dime.xs))
|
|
||||||
Text(
|
|
||||||
product.description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(MaterialTheme.dime.md))
|
|
||||||
PrimaryButton(text = "خرید", height = 44, isLoading = isPurchasing, onClick = onBuy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** جایگیرندهی shimmer برای کارت محصول هنگام بارگذاری. */
|
|
||||||
@Composable
|
|
||||||
private fun ProductCardSkeleton() {
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.width(120.dp)
|
|
||||||
.height(18.dp)
|
|
||||||
.shimmer()
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.width(72.dp)
|
|
||||||
.height(18.dp)
|
|
||||||
.shimmer()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(MaterialTheme.dime.md))
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(44.dp)
|
|
||||||
.shimmer(shape = MaterialTheme.shapes.medium)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** قالببندی قیمت با جداکنندهی هزارگان. */
|
|
||||||
private fun formatPrice(price: Long): String =
|
|
||||||
price.toString().reversed().chunked(3).joinToString(",").reversed()
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.approagency.pharmacy.presentation.account
|
||||||
|
|
||||||
|
/**
|
||||||
|
* قراردادِ شروع/توقفِ خودکارپُرکُنِ کدِ پیامک.
|
||||||
|
*
|
||||||
|
* اکتیویتی میزبان آن را پیادهسازی میکند؛ شیتِ ورود فقط به این اینترفیس وابسته
|
||||||
|
* است و از نوعِ مشخصِ اکتیویتی بیخبر میماند.
|
||||||
|
*/
|
||||||
|
interface OtpAutofillController {
|
||||||
|
fun startOtpAutofill()
|
||||||
|
fun stopOtpAutofill()
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package com.approagency.pharmacy.presentation.account
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.approagency.pharmacy.domain.model.SubscriptionProduct
|
||||||
|
import com.approagency.pharmacy.presentation.common.PrimaryButton
|
||||||
|
import com.approagency.pharmacy.presentation.common.shimmer
|
||||||
|
import com.vada.caller.ui.theme.dime
|
||||||
|
|
||||||
|
/** کارت یک محصول اشتراک بههمراه دکمهی خرید. */
|
||||||
|
@Composable
|
||||||
|
fun SubscriptionProductCard(
|
||||||
|
product: SubscriptionProduct,
|
||||||
|
isPurchasing: Boolean,
|
||||||
|
onBuy: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(product.title, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
"${formatPrice(product.price)} تومان",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!product.description.isNullOrBlank()) {
|
||||||
|
Spacer(Modifier.height(MaterialTheme.dime.xs))
|
||||||
|
Text(
|
||||||
|
product.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
|
PrimaryButton(text = "خرید", height = 44, isLoading = isPurchasing, onClick = onBuy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جایگیرندهی shimmer برای کارت محصول هنگام بارگذاری. */
|
||||||
|
@Composable
|
||||||
|
fun SubscriptionProductCardSkeleton() {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(120.dp)
|
||||||
|
.height(18.dp)
|
||||||
|
.shimmer()
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(72.dp)
|
||||||
|
.height(18.dp)
|
||||||
|
.shimmer()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(44.dp)
|
||||||
|
.shimmer(shape = MaterialTheme.shapes.medium)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** قالببندی قیمت با جداکنندهی هزارگان. */
|
||||||
|
private fun formatPrice(price: Long): String =
|
||||||
|
price.toString().reversed().chunked(3).joinToString(",").reversed()
|
||||||
+61
-38
@@ -1,75 +1,98 @@
|
|||||||
package com.approagency.pharmacy.presentation.components
|
package com.approagency.pharmacy.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocalPharmacy
|
||||||
|
import androidx.compose.material.icons.filled.Medication
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
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.ui.Alignment
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||||
import com.vada.caller.ui.theme.LocalDime
|
|
||||||
import com.vada.caller.ui.theme.dime
|
import com.vada.caller.ui.theme.dime
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DaroYabSearchResult(
|
fun DaroYabSearchResult(
|
||||||
drug: DrugSearchResult,
|
drug: DrugSearchResult,
|
||||||
onClickDetail: (DrugSearchResult) -> Unit,
|
onClickDetail: (DrugSearchResult) -> Unit,
|
||||||
onClickDrugStore:(DrugSearchResult) ->Unit,
|
onClickDrugStore: (DrugSearchResult) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
CustomBox(
|
||||||
|
|
||||||
CustomBox (
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(bottom = MaterialTheme.dime.sm)
|
.padding(bottom = MaterialTheme.dime.sm)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClickDetail(drug) },
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Row (
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dime.lg),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.Center,
|
.fillMaxWidth()
|
||||||
horizontalAlignment = Alignment.End
|
.padding(MaterialTheme.dime.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xs)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = drug.persianName,
|
text = drug.persianName,
|
||||||
style = MaterialTheme.typography.labelLarge
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
drug.englishName?.let {
|
drug.englishName?.takeIf { it.isNotBlank() }?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = 12.sp,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
text = "کد: ${drug.genericId}",
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
fontSize = 10.sp,
|
Row(
|
||||||
color = MaterialTheme.colorScheme.primary
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
|
||||||
Row(
|
) {
|
||||||
modifier = modifier.fillMaxWidth(),
|
Button(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
onClick = { onClickDrugStore(drug) },
|
||||||
) {
|
modifier = Modifier
|
||||||
TextButton(onClick = {onClickDetail(drug)} , content = {
|
.weight(1f)
|
||||||
Text("اطلاعات دارویی")
|
.height(44.dp)
|
||||||
})
|
) {
|
||||||
TextButton(onClick = {onClickDrugStore(drug)} , content = {
|
Icon(
|
||||||
Text("موجودی داروخانه")
|
imageVector = Icons.Filled.LocalPharmacy,
|
||||||
})
|
contentDescription = null,
|
||||||
}
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(MaterialTheme.dime.xs))
|
||||||
|
Text("موجودی داروخانه", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onClickDetail(drug) },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(44.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Medication,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(MaterialTheme.dime.xs))
|
||||||
|
Text("اطلاعات دارویی", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-128
@@ -3,13 +3,13 @@ package com.approagency.pharmacy.presentation.components
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -33,161 +33,152 @@ fun PharmacyBottomSheet(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = false, confirmValueChange = { true })
|
skipPartiallyExpanded = true, confirmValueChange = { true }
|
||||||
|
)
|
||||||
|
|
||||||
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
||||||
var showProvinceDropdown by remember { mutableStateOf(false) }
|
var showProvinceDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val pharmacyState by viewModel.state.collectAsState()
|
val pharmacyState by viewModel.state.collectAsState()
|
||||||
|
|
||||||
// جستجوی اولیه با استان تهران — با تغییر دارو دوباره اجرا میشود
|
// شیت بلند ثابت میشود تا لیست داخلی بهدرستی اسکرول شود (نه اینکه درگِ شیت
|
||||||
// (در صورت خالی بودن شناسه، ViewModel وضعیت قبلی را پاک و خطا نمایش میدهد)
|
// جای اسکرولِ لیست را بگیرد) و حالتهای لودینگ/خالی/خطا وسط دیده شوند.
|
||||||
|
val sheetHeight = (LocalConfiguration.current.screenHeightDp * 0.85f).dp
|
||||||
|
|
||||||
LaunchedEffect(genericDrugId, brandIrc) {
|
LaunchedEffect(genericDrugId, brandIrc) {
|
||||||
viewModel.search(genericDrugId, brandIrc, "40")
|
viewModel.search(genericDrugId, brandIrc, "40")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(sheetState = sheetState, onDismiss = onDismiss, scope = scope) {
|
||||||
sheetState = sheetState, onDismiss = onDismiss, scope = scope
|
|
||||||
) {
|
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 400.dp)
|
.height(sheetHeight)
|
||||||
.padding(MaterialTheme.dime.md),
|
.padding(MaterialTheme.dime.lg)
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xl)
|
|
||||||
) {
|
) {
|
||||||
item {
|
// سربرگ ثابت
|
||||||
Text(
|
Text(
|
||||||
text = "جستجوی دارو در داروخانهها",
|
text = "موجودی دارو در داروخانهها",
|
||||||
fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// عنوان
|
|
||||||
|
|
||||||
item {
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = showProvinceDropdown,
|
ProvinceSelector(
|
||||||
onExpandedChange = { showProvinceDropdown = it }) {
|
selectedProvinceId = selectedProvinceId,
|
||||||
OutlinedTextField(
|
expanded = showProvinceDropdown,
|
||||||
value = provinces.find { it.id == selectedProvinceId }?.name
|
onExpandedChange = { showProvinceDropdown = it },
|
||||||
?: "انتخاب استان",
|
onProvinceSelected = { id ->
|
||||||
onValueChange = {},
|
selectedProvinceId = id
|
||||||
readOnly = true,
|
showProvinceDropdown = false
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) },
|
viewModel.search(genericDrugId, brandIrc, id)
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxWidth()
|
)
|
||||||
.menuAnchor(),
|
|
||||||
shape = MaterialTheme.shapes.large
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
|
|
||||||
|
// ناحیهی محتوا فضای باقیمانده را پر میکند؛ سربرگ بالا ثابت میماند.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
when (val state = pharmacyState) {
|
||||||
|
PharmacyState.Loading -> Loading(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
message = "در حال جستجو..."
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
is PharmacyState.Success -> {
|
||||||
expanded = showProvinceDropdown,
|
if (state.items.isEmpty()) {
|
||||||
onDismissRequest = { showProvinceDropdown = false }) {
|
EmptyMessage("هیچ داروخانهای یافت نشد")
|
||||||
provinces.forEach { province ->
|
} else {
|
||||||
DropdownMenuItem(text = { Text(province.name) }, onClick = {
|
LazyColumn(
|
||||||
selectedProvinceId = province.id
|
modifier = Modifier.fillMaxSize(),
|
||||||
showProvinceDropdown = false
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
|
||||||
// جستجو با استان جدید
|
|
||||||
viewModel.search(genericDrugId, brandIrc, province.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// انتخاب استان
|
|
||||||
|
|
||||||
|
|
||||||
// نمایش نتایج
|
|
||||||
when (val state = pharmacyState) {
|
|
||||||
PharmacyState.Loading -> {
|
|
||||||
item {
|
|
||||||
Loading(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(400.dp),
|
|
||||||
message = "در حال جستجو..."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PharmacyState.Success -> {
|
|
||||||
if (state.items.isEmpty()) {
|
|
||||||
item {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(400.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
items(state.items) { pharmacy ->
|
||||||
text = "هیچ داروخانهای یافت نشد",
|
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
}
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
item {
|
||||||
)
|
Text(
|
||||||
}
|
text = "تعداد کل: ${state.items.size} داروخانه",
|
||||||
}
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
} else {
|
modifier = Modifier.padding(vertical = MaterialTheme.dime.sm)
|
||||||
items(state.items) { pharmacy ->
|
)
|
||||||
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
|
||||||
Text(
|
|
||||||
text = "تعداد کل: ${state.items.size} داروخانه",
|
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
|
||||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.xs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// LazyColumn(
|
|
||||||
// verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
// modifier = Modifier,
|
|
||||||
// ) {
|
|
||||||
// items(state.items) { pharmacy ->
|
|
||||||
// PharmacyResultItem(pharmacy = pharmacy)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PharmacyState.Error -> {
|
|
||||||
item {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
text = state.message,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
|
||||||
TextButton(
|
|
||||||
onClick = { viewModel.retry() }) {
|
|
||||||
Text("تلاش مجدد")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
is PharmacyState.Error -> Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.message,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
|
TextButton(onClick = { viewModel.retry() }) { Text("تلاش مجدد") }
|
||||||
|
}
|
||||||
|
|
||||||
PharmacyState.Idle -> {}
|
PharmacyState.Idle -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ProvinceSelector(
|
||||||
|
selectedProvinceId: String,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
onProvinceSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = onExpandedChange
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = provinces.find { it.id == selectedProvinceId }?.name ?: "انتخاب استان",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("استان") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { onExpandedChange(false) }
|
||||||
|
) {
|
||||||
|
provinces.forEach { province ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(province.name) },
|
||||||
|
onClick = { onProvinceSelected(province.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyMessage(message: String) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,8 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
repository.subscribe(product.id, result.purchaseToken, gateway.name).fold(
|
repository.subscribe(product.id, result.purchaseToken, gateway.name).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
// پس از تأییدِ سرور، خرید را مصرف کن تا دورهٔ بعد قابل خرید باشد.
|
||||||
|
gateway.consume(result.purchaseToken)
|
||||||
_ui.update {
|
_ui.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
purchasingProductId = null,
|
purchasingProductId = null,
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class HomeViewModel (
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val result = getDrugSearchUseCase.invoke(drugSearchParams)
|
val result = getDrugSearchUseCase.invoke(drugSearchParams)
|
||||||
println(result)
|
|
||||||
_uiState.update { it.copy(
|
_uiState.update { it.copy(
|
||||||
drugSearchState = it.drugSearchState.copy(
|
drugSearchState = it.drugSearchState.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -74,10 +73,8 @@ class HomeViewModel (
|
|||||||
|
|
||||||
) }
|
) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println(e.message)
|
|
||||||
handleError(e.toUserMessage())
|
handleError(e.toUserMessage())
|
||||||
}catch (e: Exception) {
|
}catch (e: Exception) {
|
||||||
println(e.message)
|
|
||||||
handleError(e.toUserMessage())
|
handleError(e.toUserMessage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +100,6 @@ class HomeViewModel (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println(result)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleError(e.toUserMessage())
|
handleError(e.toUserMessage())
|
||||||
}catch (e: Exception) {
|
}catch (e: Exception) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.approagency.pharmacy.domain.model.ThemeMode
|
||||||
import com.fabirt.podcastapp.ui.theme.Shapes
|
import com.fabirt.podcastapp.ui.theme.Shapes
|
||||||
import com.vada.caller.ui.theme.Dime
|
import com.vada.caller.ui.theme.Dime
|
||||||
import com.vada.caller.ui.theme.LocalDime
|
import com.vada.caller.ui.theme.LocalDime
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.approagency.pharmacy.utils
|
package com.approagency.pharmacy.utils
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
const val BASE_URL = "https://drug.approagency.ir/api/"
|
|
||||||
const val Darro_Url = "https://www.darooyab.ir/"
|
const val Darro_Url = "https://www.darooyab.ir/"
|
||||||
|
|
||||||
/** بکاند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */
|
/** بکاند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">داروخانه همراه</string>
|
<string name="app_name">داروچی</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# شرایط و قوانین استفاده از اپلیکیشن داروچی
|
||||||
|
|
||||||
|
آخرین بهروزرسانی: ۱۴۰۵/۰۳/۲۴
|
||||||
|
|
||||||
|
استفاده از اپلیکیشن «داروچی» (که از این پس «برنامه» نامیده میشود) بهمنزلهٔ
|
||||||
|
مطالعه و پذیرشِ کاملِ شرایط و قوانین زیر است. در صورتی که با هر بخشی از این
|
||||||
|
قوانین موافق نیستید، لطفاً از برنامه استفاده نکنید.
|
||||||
|
|
||||||
|
## ۱. معرفی خدمات
|
||||||
|
|
||||||
|
برنامه ابزاری اطلاعرسانی است که امکانات زیر را در اختیار کاربر قرار میدهد:
|
||||||
|
|
||||||
|
- جستوجوی دارو و مشاهدهٔ اطلاعات دارویی (موارد مصرف، مقدار مصرف، عوارض،
|
||||||
|
تداخلات، هشدارها و موارد مشابه).
|
||||||
|
- مشاهدهٔ موجودی دارو در داروخانهها.
|
||||||
|
- مشاهدهٔ اطلاعات آزمایشها و تستهای پزشکی.
|
||||||
|
|
||||||
|
این خدمات صرفاً جنبهٔ آموزشی و اطلاعرسانی دارند.
|
||||||
|
|
||||||
|
## ۲. سلب مسئولیت پزشکی (بسیار مهم)
|
||||||
|
|
||||||
|
- اطلاعاتِ ارائهشده در برنامه **جایگزینِ مشاورهٔ پزشک، داروساز یا کادر درمان
|
||||||
|
نیست** و نباید مبنای تشخیص، تجویز یا قطع/شروع مصرف دارو قرار گیرد.
|
||||||
|
- مصرفِ هر دارو باید **زیر نظر پزشک یا داروساز** و مطابق نسخه و دستور ایشان
|
||||||
|
انجام شود.
|
||||||
|
- اطلاعاتِ دارویی ممکن است کامل، بهروز یا بدون خطا نباشد. مسئولیتِ هرگونه
|
||||||
|
استفاده از این اطلاعات بر عهدهٔ کاربر است.
|
||||||
|
- موجودیِ نمایشدادهشدهٔ داروها در داروخانهها بر اساس دادههای منابعِ ثالث
|
||||||
|
است و ممکن است با وضعیتِ واقعی متفاوت باشد؛ پیش از مراجعه، موجودی را از
|
||||||
|
داروخانه تأیید کنید.
|
||||||
|
- در شرایطِ اورژانسی یا بروزِ عوارض، فوراً به پزشک یا اورژانس مراجعه کنید.
|
||||||
|
|
||||||
|
## ۳. حساب کاربری و ورود
|
||||||
|
|
||||||
|
- ورود به برنامه از طریق شماره موبایل و کدِ یکبارمصرف (OTP) انجام میشود.
|
||||||
|
- مسئولیتِ حفظِ شماره موبایل، دسترسی به دستگاه و کدِ تأیید بر عهدهٔ کاربر است.
|
||||||
|
- کاربر موظف است اطلاعاتِ صحیح ارائه دهد و حساب را در اختیار دیگران قرار ندهد.
|
||||||
|
|
||||||
|
## ۴. جستوجوی رایگان، اشتراک و پرداخت
|
||||||
|
|
||||||
|
- برنامه تعدادی **جستوجوی رایگان** در اختیار کاربر قرار میدهد. پس از پایانِ
|
||||||
|
سهمیهٔ رایگان، برای ادامهٔ استفادهٔ نامحدود، تهیهٔ **اشتراک** لازم است.
|
||||||
|
- خرید و پرداختِ اشتراک از طریق **درگاهِ پرداختِ درونبرنامهایِ فروشگاه
|
||||||
|
(مایکت)** انجام میشود و تابعِ قوانین و شرایطِ همان فروشگاه است.
|
||||||
|
- قیمت و مدتِ اشتراک پیش از خرید به کاربر نمایش داده میشود.
|
||||||
|
- **بازگشتِ وجه** تابعِ سیاستهای فروشگاهِ مایکت است. درخواستهای مربوط به
|
||||||
|
عودتِ وجهِ خریدهای درونبرنامهای از طریق همان فروشگاه پیگیری میشود.
|
||||||
|
- در صورتی که پس از پرداختِ موفق، اشتراک فعال نشد، با پشتیبانیِ ما تماس بگیرید.
|
||||||
|
|
||||||
|
## ۵. حریم خصوصی و دادهها
|
||||||
|
|
||||||
|
- برای ارائهٔ خدمات، حداقلِ اطلاعاتِ لازم (از جمله **شماره موبایل** و وضعیتِ
|
||||||
|
اشتراک) دریافت و نگهداری میشود.
|
||||||
|
- اطلاعاتِ خرید (توکنِ خرید) صرفاً برای فعالسازیِ اشتراک به سرور ارسال میشود.
|
||||||
|
- اطلاعاتِ کاربران بدون رضایتِ ایشان در اختیارِ اشخاصِ ثالثِ غیرمرتبط قرار
|
||||||
|
نمیگیرد، مگر در مواردی که قانون الزام کند.
|
||||||
|
- برای آگاهیِ بیشتر، به سند **حریم خصوصی** برنامه مراجعه کنید.
|
||||||
|
|
||||||
|
## ۶. تعهدات کاربر
|
||||||
|
|
||||||
|
کاربر متعهد میشود که:
|
||||||
|
|
||||||
|
- از برنامه صرفاً برای اهدافِ قانونی و شخصی استفاده کند.
|
||||||
|
- در عملکردِ برنامه اخلال ایجاد نکند و از روشهای نفوذ، مهندسیِ معکوس یا دورزدنِ
|
||||||
|
محدودیتها استفاده نکند.
|
||||||
|
- محتوای برنامه را بدون اجازه بازنشر یا تجاریسازی نکند.
|
||||||
|
|
||||||
|
## ۷. مالکیت فکری
|
||||||
|
|
||||||
|
تمامِ حقوقِ مربوط به برنامه، نام، نشان (لوگو)، رابط کاربری و محتوای آن متعلق به
|
||||||
|
مالکِ برنامه است و هرگونه استفادهٔ غیرمجاز پیگردِ قانونی دارد.
|
||||||
|
|
||||||
|
## ۸. محدودیت مسئولیت
|
||||||
|
|
||||||
|
- برنامه «همانگونه که هست» ارائه میشود و هیچ تضمینی نسبت به بینقص بودن،
|
||||||
|
دردسترس بودنِ دائمی یا صحتِ کاملِ اطلاعات داده نمیشود.
|
||||||
|
- مالکِ برنامه در قبالِ خساراتِ مستقیم یا غیرمستقیمِ ناشی از استفاده یا عدمِ
|
||||||
|
امکانِ استفاده از برنامه، یا اتکا به اطلاعاتِ آن، مسئولیتی نمیپذیرد.
|
||||||
|
|
||||||
|
## ۹. تعلیق و خاتمه
|
||||||
|
|
||||||
|
در صورتِ نقضِ این قوانین، دسترسیِ کاربر به برنامه ممکن است بدونِ اطلاعِ قبلی
|
||||||
|
محدود یا مسدود شود.
|
||||||
|
|
||||||
|
## ۱۰. تغییر قوانین
|
||||||
|
|
||||||
|
این شرایط ممکن است در آینده بهروزرسانی شود. ادامهٔ استفاده از برنامه پس از
|
||||||
|
انتشارِ نسخهٔ جدید، بهمنزلهٔ پذیرشِ تغییرات است.
|
||||||
|
|
||||||
|
## ۱۱. قانونِ حاکم
|
||||||
|
|
||||||
|
این شرایط تابعِ قوانینِ جمهوریِ اسلامیِ ایران است و اختلافاتِ احتمالی مطابقِ همین
|
||||||
|
قوانین حلوفصل میشود.
|
||||||
|
|
||||||
|
## ۱۲. ارتباط با ما
|
||||||
|
|
||||||
|
در صورتِ هرگونه پرسش یا مشکل، از طریقِ راههای زیر با ما در تماس باشید:
|
||||||
|
|
||||||
|
- ایمیل پشتیبانی: approagency@gmail.com
|
||||||
@@ -18,6 +18,7 @@ dependencyResolutionManagement {
|
|||||||
//google()
|
//google()
|
||||||
//mavenCentral()
|
//mavenCentral()
|
||||||
maven { url = uri("https://maven.myket.ir") }
|
maven { url = uri("https://maven.myket.ir") }
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user