diff --git a/.run/iosApp.run.xml b/.run/iosApp.run.xml index 5b54005..11e4831 100644 --- a/.run/iosApp.run.xml +++ b/.run/iosApp.run.xml @@ -1,5 +1,5 @@ - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 291ab4d..2a7d491 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ sqldelight = "2.0.0" google-services = "4.4.0" # Libraries -compose-compiler = "1.5.6" +compose-compiler = "1.5.7" accompanist-permissions = "0.32.0" activity-compose = "1.8.2" appcompat = "1.6.1" diff --git a/shared/src/androidUnitTest/kotlin/me/sujanpoudel/playdeals/common/ui/screens/HomeScreenViewModelTest.kt b/shared/src/androidUnitTest/kotlin/me/sujanpoudel/playdeals/common/ui/screens/HomeScreenViewModelTest.kt index c61a7b8..70a8156 100644 --- a/shared/src/androidUnitTest/kotlin/me/sujanpoudel/playdeals/common/ui/screens/HomeScreenViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/me/sujanpoudel/playdeals/common/ui/screens/HomeScreenViewModelTest.kt @@ -8,9 +8,7 @@ import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -23,6 +21,8 @@ import me.sujanpoudel.playdeals.common.domain.models.Failure import me.sujanpoudel.playdeals.common.domain.models.Result import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences import me.sujanpoudel.playdeals.common.domain.repositories.DealsRepository +import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository +import me.sujanpoudel.playdeals.common.domain.repositories.defaultUsdConversion import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenState import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenViewModel import me.sujanpoudel.playdeals.common.utils.setMainDispatcher @@ -30,15 +30,41 @@ import me.sujanpoudel.playdeals.common.utils.settings.asObservableSettings import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +fun dealEntity(id: String = "1") = DealEntity( + id = id, + name = "name", + icon = "icon", + images = listOf(), + normalPrice = 1.1f, + currentPrice = 1.2f, + currency = "$", + url = "", + category = "game", + downloads = "150k", + rating = 3.7f, + offerExpiresIn = kotlinx.datetime.Clock.System.now(), + type = "", + source = "", + createdAt = kotlinx.datetime.Clock.System.now(), + updatedAt = kotlinx.datetime.Clock.System.now(), +) + @OptIn(ExperimentalCoroutinesApi::class) class HomeScreenViewModelTest { @MockK lateinit var dealsRepository: DealsRepository + @MockK + lateinit var forexRepository: ForexRepository + @BeforeEach fun setup() { MockKAnnotations.init(this) + coEvery { forexRepository.refreshRatesIfNecessary() } returns Result.success(Unit) + coEvery { forexRepository.forexRatesFlow() } returns flowOf(emptyList()) + coEvery { forexRepository.forexUpdateAtFlow() } returns MutableStateFlow(null) + coEvery { forexRepository.preferredConversionRateFlow() } returns MutableStateFlow(defaultUsdConversion()) } private fun appPreference(): AppPreferences { @@ -62,7 +88,7 @@ class HomeScreenViewModelTest { coEvery { dealsRepository.dealsFlow() } returns emptyFlow() coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) - HomeScreenViewModel(appPreference(), dealsRepository) + HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) coVerify { dealsRepository.dealsFlow() @@ -76,21 +102,17 @@ class HomeScreenViewModelTest { coEvery { dealsRepository.dealsFlow() } returns emptyFlow() coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) - HomeScreenViewModel(appPreference(), dealsRepository) + HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) coVerify { dealsRepository.refreshDeals() } } @Test fun `should correctly update state before calling the remoteAPI_getDeals`(): Unit = runTest { - val dispatcher = StandardTestDispatcher() - - setMainDispatcher(dispatcher) - coEvery { dealsRepository.dealsFlow() } returns emptyFlow() coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) viewModel.state.value.let { it.allDeals shouldHaveSize 0 @@ -108,7 +130,7 @@ class HomeScreenViewModelTest { coEvery { dealsRepository.dealsFlow() } returns emptyFlow() coEvery { dealsRepository.refreshDeals() } returns Result.failure(Failure.UnknownError) - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) dispatcher.scheduler.runCurrent() @@ -125,16 +147,12 @@ class HomeScreenViewModelTest { runTest { val dispatcher = StandardTestDispatcher() - val deal = mockk() - - every { deal.category } returns "" - setMainDispatcher(dispatcher) coEvery { dealsRepository.dealsFlow() } returns emptyFlow() coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) dispatcher.scheduler.runCurrent() @@ -151,15 +169,14 @@ class HomeScreenViewModelTest { runTest { val dispatcher = StandardTestDispatcher() - val entity = mockk() - every { entity.category } returns "" + val entity = dealEntity("32") setMainDispatcher(dispatcher) coEvery { dealsRepository.dealsFlow() } returns flowOf(listOf(entity)) coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) dispatcher.scheduler.runCurrent() @@ -184,7 +201,7 @@ class HomeScreenViewModelTest { coEvery { dealsRepository.dealsFlow() } returns emptyFlow() - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) viewModel.state.value.also { state -> state.allDeals shouldHaveSize 0 @@ -230,9 +247,7 @@ class HomeScreenViewModelTest { runTest { val dispatcher = StandardTestDispatcher() - val entity = mockk() - - every { entity.category } returns "" + val entity = dealEntity("1") setMainDispatcher(dispatcher) @@ -242,7 +257,7 @@ class HomeScreenViewModelTest { coEvery { dealsRepository.dealsFlow() } returns flowOf(listOf(entity)) - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) viewModel.state.value.also { state -> state.allDeals shouldHaveSize 0 @@ -289,20 +304,16 @@ class HomeScreenViewModelTest { val dispatcher = StandardTestDispatcher() setMainDispatcher(dispatcher) - val deal1 = mockk() - val deal2 = mockk() + val deal1 = dealEntity("123") + val deal2 = dealEntity("321") val flow = MutableStateFlow>(emptyList()) - every { deal1.category } returns "" - every { deal2.category } returns "" - coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit) coEvery { dealsRepository.dealsFlow() } returns flow - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) - dispatcher.scheduler.advanceTimeBy(10000) dispatcher.scheduler.advanceUntilIdle() viewModel.state.value.also { state -> @@ -312,6 +323,7 @@ class HomeScreenViewModelTest { flow.emit(listOf(deal1, deal2)) dispatcher.scheduler.advanceUntilIdle() + dispatcher.scheduler.runCurrent() viewModel.state.value.also { state -> state.allDeals.shouldContainExactly(deal1, deal2) @@ -324,18 +336,15 @@ class HomeScreenViewModelTest { val dispatcher = StandardTestDispatcher() setMainDispatcher(dispatcher) - val deal1 = mockk() - val deal2 = mockk() + val deal1 = dealEntity() + val deal2 = dealEntity("2") val flow = MutableStateFlow>(emptyList()) - every { deal1.category } returns "" - every { deal2.category } returns "" - coEvery { dealsRepository.refreshDeals() } returns Result.failure(Failure.UnknownError) coEvery { dealsRepository.dealsFlow() } returns flow - val viewModel = HomeScreenViewModel(appPreference(), dealsRepository) + val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository) dispatcher.scheduler.advanceTimeBy(10000) dispatcher.scheduler.advanceUntilIdle() diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/Constants.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/Constants.kt index 00dc400..5582a8a 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/Constants.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/Constants.kt @@ -1,7 +1,12 @@ package me.sujanpoudel.playdeals.common +import kotlin.time.Duration.Companion.hours + object Constants { const val API_BASE_URL = "https://api.play-deals.contabo.sujanpoudel.me/api" const val ABOUT_ME_URL = "https://sujanpoudel.me" val DATABASE_NAME = "${BuildKonfig.PACKAGE_NAME}.db" + + val FOREX_REFRESH_DURATION = 5.hours + const val BUNDLED_FOREX = "raw/forex.json" } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/persistent.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/persistent.kt index 5cc3849..5c82864 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/persistent.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/persistent.kt @@ -6,6 +6,7 @@ import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences import me.sujanpoudel.playdeals.common.domain.persistent.db.buildDealAdaptor import me.sujanpoudel.playdeals.common.domain.persistent.db.createSqlDriver import me.sujanpoudel.playdeals.common.domain.repositories.DealsRepository +import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository import me.sujanpoudel.playdeals.common.utils.settings.asObservableSettings import org.kodein.di.DI import org.kodein.di.bindSingleton @@ -30,4 +31,11 @@ internal val persistentModule = DI.Module("persistent") { appPreferences = instance(), ) } + bindSingleton { + ForexRepository( + remoteAPI = instance(), + appPreferences = instance(), + database = instance(), + ) + } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt index 2e9c4c4..f53beaa 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt @@ -13,13 +13,14 @@ internal val viewModelModule = DI.Module("viewModel") { bindProvider { HomeScreenViewModel( appPreferences = instance(), - repository = instance(), + dealsRepository = instance(), + forexRepository = instance(), ) } bindProvider { NewDealScreenViewModel() } - bindProvider { SettingsScreenViewModel(instance()) } + bindProvider { SettingsScreenViewModel(instance(), instance()) } bindProvider { NotificationSettingsScreenViewModel(instance()) } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/DealEntity.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/DealEntity.kt index fb9b5a3..7d1ff75 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/DealEntity.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/DealEntity.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable import kotlinx.datetime.Clock import kotlinx.datetime.Instant import me.sujanpoudel.playdeals.common.strings.Strings -import me.sujanpoudel.playdeals.common.utils.asCurrencySymbol import me.sujanpoudel.playdeals.common.utils.formatAsPrice import me.sujanpoudel.playdeals.common.utils.shallowFormatted @@ -31,14 +30,14 @@ data class DealEntity( val ratingFormatted: String get() = rating.formatAsPrice() - fun formattedNormalPrice() = "${currency.asCurrencySymbol()}${normalPrice.formatAsPrice()}" + fun formattedNormalPrice() = "${currency}${normalPrice.formatAsPrice()}" @Composable fun formattedCurrentPrice() = if (currentPrice == 0f) { Strings.free } else { - "${currency.asCurrencySymbol()}${currentPrice.formatAsPrice()}" + "${currency}${currentPrice.formatAsPrice()}" } @Composable diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/ForexEntity.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/ForexEntity.kt new file mode 100644 index 0000000..57cfbb9 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/ForexEntity.kt @@ -0,0 +1,18 @@ +package me.sujanpoudel.playdeals.common.domain.entities + +import androidx.compose.runtime.Immutable +import kotlinx.datetime.Instant + +@Immutable +data class Forex( + val timestamp: Instant, + val rates: List, +) + +@Immutable +data class ForexRateEntity( + val currency: String, + val symbol: String, + val name: String, + val rate: Float, +) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/models/api/ForexModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/models/api/ForexModel.kt new file mode 100644 index 0000000..fc73c87 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/models/api/ForexModel.kt @@ -0,0 +1,29 @@ +package me.sujanpoudel.playdeals.common.domain.models.api + +import androidx.compose.runtime.Immutable +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity + +@Serializable +@Immutable +data class ForexModel( + val timestamp: Instant, + val rates: List, +) + +@Serializable +@Immutable +data class ForexRate( + val currency: String, + val symbol: String, + val name: String, + val rate: Float, +) + +fun ForexRate.toEntity() = ForexRateEntity( + currency = currency, + symbol = symbol, + name = name, + rate = rate, +) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/networking/RemoteAPI.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/networking/RemoteAPI.kt index 4b344d5..659f529 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/networking/RemoteAPI.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/networking/RemoteAPI.kt @@ -2,6 +2,7 @@ package me.sujanpoudel.playdeals.common.domain.networking import io.ktor.client.HttpClient import me.sujanpoudel.playdeals.common.domain.models.api.AppDealModel +import me.sujanpoudel.playdeals.common.domain.models.api.ForexModel import me.sujanpoudel.playdeals.common.domain.models.api.toEntity import me.sujanpoudel.playdeals.common.domain.models.map @@ -13,4 +14,6 @@ class RemoteAPI( deal.toEntity() } } + + suspend fun getForexRates() = client.get("/forex") } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt index 7bb2f6d..32c7783 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt @@ -6,7 +6,9 @@ import kotlinx.datetime.Instant import me.sujanpoudel.playdeals.common.strings.AppLanguage import me.sujanpoudel.playdeals.common.ui.theme.AppearanceMode import me.sujanpoudel.playdeals.common.utils.settings.boolSettingState +import me.sujanpoudel.playdeals.common.utils.settings.nullableStringBackedSettingState import me.sujanpoudel.playdeals.common.utils.settings.stringBackedSettingState +import me.sujanpoudel.playdeals.common.utils.settings.stringSettingState class AppPreferences(private val settings: ObservableSettings) { private object Keys { @@ -20,6 +22,8 @@ class AppPreferences(private val settings: ObservableSettings) { const val PREFERRED_LANGUAGE = "PREFERRED_LANGUAGE" const val LAST_UPDATED_TIME = "LAST_UPDATED_TIME" const val CHANGELOG_SHOWN_FOR_VERSION = "CHANGELOG_SHOWN_FOR_VERSION" + const val FOREX_RATES_AT = "FOREX_RATE_AT" + const val PREFERRED_CURRENCY = "PREFERRED_CURRENCY" } val appearanceMode = settings.stringBackedSettingState( @@ -40,6 +44,14 @@ class AppPreferences(private val settings: ObservableSettings) { fromString = { Instant.parse(it) }, ) + val forexRateAt = settings.nullableStringBackedSettingState( + Keys.FOREX_RATES_AT, + toString = Instant::toString, + fromString = Instant::parse, + ) + + val preferredCurrency = settings.stringSettingState(Keys.PREFERRED_CURRENCY, "USD") + val developerMode = settings.boolSettingState(Keys.DEVELOPER_MODE, false) val subscribeToFreeDeals = settings.boolSettingState(Keys.NEW_DEAL_NOTIFICATION, true) val subscribeToDiscountDeals = settings.boolSettingState(Keys.NEW_DISCOUNT_DEAL_NOTIFICATION, true) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/ForexQueries.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/ForexQueries.kt new file mode 100644 index 0000000..e0831ec --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/ForexQueries.kt @@ -0,0 +1,36 @@ +package me.sujanpoudel.playdeals.common.domain.persistent.db + +import app.cash.sqldelight.SuspendingTransactionWithoutReturn +import me.sujanpoudel.playdeals.common.ForexQueries +import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity +import migrations.ForexRate + +suspend fun ForexQueries.upsert(entity: ForexRateEntity) { + with(entity) { + upsert( + name = name, + currency = currency, + symbol = symbol, + rate = rate.toDouble(), + ) + } +} + +suspend fun ForexQueries.upsertAll( + deals: List, + runInTnx: suspend SuspendingTransactionWithoutReturn.() -> Unit = {}, +) { + transaction { + deals.forEach { + upsert(it) + } + runInTnx() + } +} + +fun ForexRate.toEntity() = ForexRateEntity( + currency = currency, + symbol = symbol, + name = name, + rate = rate.toFloat(), +) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/repositories/ForexRepository.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/repositories/ForexRepository.kt new file mode 100644 index 0000000..399f64f --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/repositories/ForexRepository.kt @@ -0,0 +1,91 @@ +package me.sujanpoudel.playdeals.common.domain.repositories + +import app.cash.sqldelight.coroutines.asFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant +import kotlinx.datetime.until +import kotlinx.serialization.json.Json +import me.sujanpoudel.playdeals.common.Constants +import me.sujanpoudel.playdeals.common.SqliteDatabase +import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity +import me.sujanpoudel.playdeals.common.domain.models.Result +import me.sujanpoudel.playdeals.common.domain.models.api.ForexModel +import me.sujanpoudel.playdeals.common.domain.models.api.toEntity +import me.sujanpoudel.playdeals.common.domain.models.map +import me.sujanpoudel.playdeals.common.domain.models.onSuccess +import me.sujanpoudel.playdeals.common.domain.networking.RemoteAPI +import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences +import me.sujanpoudel.playdeals.common.domain.persistent.db.toEntity +import me.sujanpoudel.playdeals.common.domain.persistent.db.upsertAll +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.resource + +fun defaultUsdConversion() = ForexRateEntity( + currency = "USD", + symbol = "$", + name = "United State Dollar", + rate = 1f, +) + +class ForexRepository( + private val remoteAPI: RemoteAPI, + private val appPreferences: AppPreferences, + private val database: SqliteDatabase, +) { + + suspend fun refreshRatesIfNecessary(): Result = withContext(Dispatchers.IO) { + val shouldRefresh = appPreferences.forexRateAt.value?.let { + val timeElapsed = it.until(Clock.System.now(), DateTimeUnit.SECOND) + timeElapsed > Constants.FOREX_REFRESH_DURATION.inWholeSeconds + } ?: let { + loadBundledForex() + true + } + + if (shouldRefresh) { + remoteAPI.getForexRates() + .onSuccess { forex -> + appPreferences.forexRateAt.update(forex.timestamp) + database.forexQueries.upsertAll(forex.rates.map { it.toEntity() }) + } + .map { } + } else { + Result.success(Unit) + } + } + + @OptIn(ExperimentalResourceApi::class) + private suspend fun loadBundledForex() { + runCatching { + val forex = Json.decodeFromString(resource(Constants.BUNDLED_FOREX).readBytes().decodeToString()) + appPreferences.forexRateAt.update(forex.timestamp) + database.forexQueries.upsertAll(forex.rates.map { it.toEntity() }) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun forexRatesFlow(): Flow> = database.forexQueries.getAll() + .asFlow() + .mapLatest { it.executeAsList() } + .map { forexRates -> forexRates.map { it.toEntity() } } + + fun forexUpdateAtFlow(): StateFlow = appPreferences.forexRateAt + + fun setPreferredConversion(forexRate: ForexRateEntity) = + appPreferences.preferredCurrency.update(forexRate.currency) + + fun preferredConversionRateFlow(): Flow = forexRatesFlow() + .combine(appPreferences.preferredCurrency) { rates, currency -> + rates.find { it.currency == currency } ?: defaultUsdConversion() + } +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/extensions/StateFlow.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/extensions/StateFlow.kt new file mode 100644 index 0000000..4eb189c --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/extensions/StateFlow.kt @@ -0,0 +1,21 @@ +package me.sujanpoudel.playdeals.common.extensions + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +/** + * Combines two [StateFlow]s into a single [StateFlow] + */ +@OptIn(DelicateCoroutinesApi::class) +fun StateFlow.combineState( + flow2: StateFlow, + scope: CoroutineScope = GlobalScope, + sharingStarted: SharingStarted = SharingStarted.Eagerly, + transform: (T1, T2) -> R, +): StateFlow = combine(this, flow2) { o1, o2 -> transform.invoke(o1, o2) } + .stateIn(scope, sharingStarted, transform.invoke(this.value, flow2.value)) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt index 3c3b63a..1eacd3f 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt @@ -29,6 +29,7 @@ interface AppStrings { val pushNotificationDescription: String val themePreference: String val chooseLanguage: String + val chooseDisplayCurrency: String val close: String // changelog @@ -52,4 +53,5 @@ interface AppStrings { val developerModeDescription: String val permissionRequired: String val grantPermission: String + val displayCurrencyDescription: String } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt index 4a9c741..3a93d31 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt @@ -29,6 +29,8 @@ object StringEn : AppStrings { override val pushNotificationDescription = "Manage when you want to be notified" override val themePreference = "Theme Preference" override val chooseLanguage = "Choose Language" + override val chooseDisplayCurrency = "Display Currency" + override val displayCurrencyDescription = "The app prices will be shown on" // Changelog Screen override val close = "Close" diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt index 7169da8..3256be1 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt @@ -30,19 +30,22 @@ import me.sujanpoudel.playdeals.common.domain.models.DealFilterOption import me.sujanpoudel.playdeals.common.domain.models.Selectable import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenState -import me.sujanpoudel.playdeals.common.ui.screens.home.filterDealsToDisplay +import me.sujanpoudel.playdeals.common.ui.screens.home.filterWith import me.sujanpoudel.playdeals.common.ui.theme.SOFT_COLOR_ALPHA object DealContent { @OptIn(ExperimentalFoundationApi::class) @Composable operator fun invoke( - homeScreenState: HomeScreenState, + state: HomeScreenState, onToggleFilterOption: (DealFilterOption) -> Unit, refreshAppDeals: () -> Unit, ) { - val deals = remember(homeScreenState.allDeals, homeScreenState.filterOptions, homeScreenState.lastUpdatedTime) { - homeScreenState.allDeals.filterDealsToDisplay(homeScreenState.filterOptions, homeScreenState.lastUpdatedTime) + val deals = remember(state.allDeals, state.filterOptions) { + state.allDeals.filterWith( + filterOptions = state.filterOptions, + lastUpdatedTime = state.lastUpdatedTime, + ) } Column( @@ -53,7 +56,7 @@ object DealContent { contentPadding = PaddingValues(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { - itemsIndexed(homeScreenState.filterOptions) { index, it -> + itemsIndexed(state.filterOptions) { index, it -> FilterChipItem( index = index, filterOption = it, @@ -74,7 +77,7 @@ object DealContent { DealItem( appDeal = deal, modifier = Modifier.animateItemPlacement(), - isAppNewlyAdded = homeScreenState.lastUpdatedTime < deal.createdAt, + isAppNewlyAdded = state.lastUpdatedTime < deal.createdAt, ) } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealItem.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealItem.kt index 1bc05df..712881e 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealItem.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealItem.kt @@ -79,11 +79,11 @@ object DealItem { } PriceButton( - normalPrice = appDeal.formattedNormalPrice(), - currentPrice = appDeal.formattedCurrentPrice(), modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 16.dp), + normalPrice = appDeal.formattedNormalPrice(), + currentPrice = appDeal.formattedCurrentPrice(), ) { uriHandler.openUri(appDeal.url) } @@ -116,8 +116,6 @@ object DealItem { AppDetail2ndRow(deal) - AppDetail3rdRow(deal) - Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, @@ -128,6 +126,8 @@ object DealItem { color = MaterialTheme.colorScheme.onBackground.copy(alpha = SOFT_COLOR_ALPHA), ) } + + AppDetail4thRow(deal) } } } @@ -235,20 +235,6 @@ object DealItem { style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onBackground.copy(alpha = SOFT_COLOR_ALPHA), ) - } - } - - @Composable - private fun AppDetail3rdRow(appDeal: DealEntity) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - appDeal.ratingFormatted, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = SOFT_COLOR_ALPHA), - ) Icon( Icons.Outlined.Star, @@ -257,6 +243,20 @@ object DealItem { tint = MaterialTheme.colorScheme.onBackground.copy(alpha = SOFT_COLOR_ALPHA), ) + Text( + appDeal.ratingFormatted, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = SOFT_COLOR_ALPHA), + ) + } + } + + @Composable + private fun AppDetail4thRow(appDeal: DealEntity) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { Text( text = appDeal.formattedExpiryInfo(), style = MaterialTheme.typography.labelMedium, @@ -284,14 +284,14 @@ object DealItem { ) { Text( text = normalPrice, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelSmall, textDecoration = TextDecoration.LineThrough, color = MaterialTheme.colorScheme.onPrimary, ) Text( text = currentPrice, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary, ) } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt index 81c0119..ef04084 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt @@ -7,9 +7,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon @@ -24,13 +28,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import kotlinx.datetime.Instant import me.sujanpoudel.playdeals.common.BuildKonfig +import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity import me.sujanpoudel.playdeals.common.extensions.capitalizeWords import me.sujanpoudel.playdeals.common.strings.AppLanguage import me.sujanpoudel.playdeals.common.strings.Strings +import me.sujanpoudel.playdeals.common.ui.components.common.clickableWithoutIndicator import me.sujanpoudel.playdeals.common.ui.screens.settings.icon import me.sujanpoudel.playdeals.common.ui.theme.AppearanceMode import me.sujanpoudel.playdeals.common.ui.theme.SOFT_COLOR_ALPHA @@ -77,6 +85,7 @@ object SettingsScreen { @Composable fun Footer( onClick: () -> Unit, + forexUpdatedAt: Instant?, color: Color, ) { Column( @@ -87,11 +96,16 @@ object SettingsScreen { horizontalAlignment = Alignment.CenterHorizontally, ) { Text(Strings.appDeals, style = MaterialTheme.typography.titleSmall, color = color) + Text( "V${BuildKonfig.VERSION_NAME}-${BuildKonfig.VERSION_CODE}", style = MaterialTheme.typography.bodySmall, color = color, ) + + forexUpdatedAt?.also { + Text("Forex at $it", style = MaterialTheme.typography.titleSmall, color = color) + } } } @@ -112,7 +126,7 @@ object SettingsScreen { } ChoiceDialogue( - title = Strings.chooseLanguage, + title = Strings.appearance, choices = remember { AppearanceMode.values().toList() }, isActive = dialogActive, closeRequest = { dialogActive = false }, @@ -152,6 +166,40 @@ object SettingsScreen { ) } + @Composable + fun CurrencySetting( + preferred: ForexRateEntity, + rates: List, + setPreferred: (ForexRateEntity) -> Unit, + ) { + var dialogActive by remember { mutableStateOf(false) } + + SettingItem( + title = Strings.chooseDisplayCurrency, + description = Strings.displayCurrencyDescription, + onClick = { dialogActive = true }, + ) { + Text( + "${preferred.currency}(${preferred.symbol})", + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + ChoiceDialogue( + title = Strings.chooseDisplayCurrency, + choices = remember(rates) { rates.sortedBy { it.name } }, + isActive = dialogActive, + closeRequest = { dialogActive = false }, + onChooseValue = setPreferred, + choiceLabelBy = { it.name }, + choiceIcon = { + Text(it.symbol, style = MaterialTheme.typography.bodyLarge) + }, + ) + } + @Composable fun ChoiceDialogue( title: String, @@ -166,9 +214,12 @@ object SettingsScreen { Dialog(closeRequest, properties = DialogProperties()) { Column( modifier = Modifier + .clickableWithoutIndicator { closeRequest() } + .windowInsetsPadding(WindowInsets(bottom = 100.dp, top = 100.dp)) .clip(RoundedCornerShape(5)) .background(MaterialTheme.colorScheme.surface) - .padding(top = 16.dp), + .padding(top = 16.dp) + .clickableWithoutIndicator { }, ) { Text( title, @@ -179,33 +230,35 @@ object SettingsScreen { Divider() - choices.forEach { - Row( - modifier = Modifier - .height(48.dp) - .fillMaxWidth() - .clickable { - closeRequest() - onChooseValue(it) - } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = choiceLabelBy(it), - modifier = Modifier.weight(1f).apply { - if (choices.last() == it) { - this.padding(bottom = 32.dp) + LazyColumn { + items(choices) { + Row( + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + .clickable { + closeRequest() + onChooseValue(it) } - }, - style = MaterialTheme.typography.titleSmall, - ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = choiceLabelBy(it), + modifier = Modifier.weight(1f).apply { + if (choices.last() == it) { + this.padding(bottom = 32.dp) + } + }, + style = MaterialTheme.typography.titleSmall, + ) - choiceIcon(it) - } + choiceIcon(it) + } - if (choices.last() != it) { - Divider() + if (choices.last() != it) { + Divider() + } } } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt index f00807c..b600a33 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt @@ -20,7 +20,7 @@ data class HomeScreenState( val destinationOneOff: Screens? = null, ) -fun List.filterDealsToDisplay( +fun List.filterWith( filterOptions: List>, lastUpdatedTime: Instant, ): List { diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt index fc83b62..70505a1 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt @@ -1,12 +1,10 @@ package me.sujanpoudel.playdeals.common.ui.screens.home import io.ktor.util.reflect.instanceOf -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.sujanpoudel.playdeals.common.BuildKonfig @@ -17,32 +15,43 @@ import me.sujanpoudel.playdeals.common.domain.models.Result import me.sujanpoudel.playdeals.common.domain.models.Selectable import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences import me.sujanpoudel.playdeals.common.domain.repositories.DealsRepository +import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository import me.sujanpoudel.playdeals.common.extensions.capitalizeWords import me.sujanpoudel.playdeals.common.viewModel.ViewModel import me.sujanpoudel.playdeals.common.viewModel.viewModelScope -import kotlin.time.Duration.Companion.milliseconds -@OptIn(ExperimentalStdlibApi::class, FlowPreview::class) +@OptIn(ExperimentalStdlibApi::class) class HomeScreenViewModel( private val appPreferences: AppPreferences, - private val repository: DealsRepository, + private val dealsRepository: DealsRepository, + private val forexRepository: ForexRepository, ) : ViewModel() { private val _state = MutableStateFlow( HomeScreenState(lastUpdatedTime = appPreferences.lastUpdatedTime.value), ) + val state = _state as StateFlow init { observeDeals() refreshDeals() checkIfChangelogNeedsToBeShown() + refreshForex() } private fun observeDeals() = viewModelScope.launch { - repository.dealsFlow() - .debounce(300.milliseconds) - .collectLatest { deals -> + dealsRepository.dealsFlow() + .combine(forexRepository.preferredConversionRateFlow()) { deals, rate -> + deals.map { deal -> + deal.copy( + currentPrice = deal.currentPrice * rate.rate, + normalPrice = deal.normalPrice * rate.rate, + currency = rate.symbol, + ) + } + } + .collect { deals -> _state.update { state -> state.copy( persistentError = if (deals.isNotEmpty()) null else state.persistentError, @@ -80,7 +89,7 @@ class HomeScreenViewModel( } viewModelScope.launch { - val result = repository.refreshDeals() + val result = dealsRepository.refreshDeals() _state.update { state -> when (result) { is Result.Error -> state.copy( @@ -101,6 +110,10 @@ class HomeScreenViewModel( } } + fun refreshForex() = viewModelScope.launch { + forexRepository.refreshRatesIfNecessary() + } + fun clearErrorOneOff() { _state.update { state -> state.copy(errorOneOff = null) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt index cc5d7d3..69b507c 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt @@ -22,6 +22,7 @@ import me.sujanpoudel.playdeals.common.pushNotification.current import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.AppearanceModeSetting +import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.CurrencySetting import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.Footer import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.LanguageSetting import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.SettingItem @@ -45,7 +46,9 @@ fun SettingsScreen() { val appearanceMode by viewModel.appearanceMode.collectAsState() val developerModeEnabled by viewModel.developerModeEnabled.collectAsState() val appLanguage by viewModel.appLanguage.collectAsState() - + val forexUpdatedAt by viewModel.forexUpdatedAt.collectAsState() + val preferredCurrency by viewModel.preferredConversion.collectAsState() + val forexRates by viewModel.forexRates.collectAsState() val navigator = Navigator.current val notificationManager = NotificationManager.current @@ -68,10 +71,13 @@ fun SettingsScreen() { LanguageSetting(appLanguage, viewModel::setAppLanguage) + CurrencySetting(preferredCurrency, forexRates, viewModel::setPreferredCurrency) + Spacer(Modifier.weight(1f)) Footer( onClick = { viewModel.setDeveloperModeEnabled(true) }, + forexUpdatedAt = forexUpdatedAt, color = if (developerModeEnabled) { MaterialTheme.colorScheme.primary } else { diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt index a81ecb0..66ac83f 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt @@ -1,18 +1,41 @@ package me.sujanpoudel.playdeals.common.ui.screens.settings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant +import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences +import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository +import me.sujanpoudel.playdeals.common.domain.repositories.defaultUsdConversion import me.sujanpoudel.playdeals.common.strings.AppLanguage import me.sujanpoudel.playdeals.common.ui.theme.AppearanceMode import me.sujanpoudel.playdeals.common.viewModel.ViewModel +import me.sujanpoudel.playdeals.common.viewModel.viewModelScope class SettingsScreenViewModel( private val appPreferences: AppPreferences, + private val forexRepository: ForexRepository, ) : ViewModel() { val appearanceMode: StateFlow = appPreferences.appearanceMode val developerModeEnabled: StateFlow = appPreferences.developerMode val appLanguage: StateFlow = appPreferences.appLanguage + val forexUpdatedAt: StateFlow = forexRepository.forexUpdateAtFlow() + val forexRates: StateFlow> = forexRepository.forexRatesFlow() + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val preferredConversion = MutableStateFlow(defaultUsdConversion()) + + init { + viewModelScope.launch { + forexRepository.preferredConversionRateFlow() + .collectLatest { preferredConversion.value = it } + } + } fun setAppearanceMode(mode: AppearanceMode) { appPreferences.appearanceMode.update(mode) @@ -25,4 +48,8 @@ class SettingsScreenViewModel( fun setAppLanguage(appLanguage: AppLanguage) { appPreferences.appLanguage.update(appLanguage) } + + fun setPreferredCurrency(rate: ForexRateEntity) { + forexRepository.setPreferredConversion(rate) + } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/formattting.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/formattting.kt index 1ffa9c3..f66d7a9 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/formattting.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/formattting.kt @@ -18,11 +18,6 @@ fun Float.formatAsPrice(): String { return "$int.$formattedDecimal" } -fun String.asCurrencySymbol() = when (this) { - "USD" -> "$" - else -> this -} - fun Duration.shallowFormatted(): String { if (inWholeDays.absoluteValue > 0) { return "${inWholeDays.absoluteValue} ${StringEn.days}" diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/CoroutineExtensions.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/CoroutineExtensions.kt index f63c071..3fc6707 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/CoroutineExtensions.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/CoroutineExtensions.kt @@ -10,9 +10,15 @@ private fun createStateFlow( getValue: (String, T) -> T, addListener: (String, T, (T) -> Unit) -> Unit, ): StateFlow = MutableStateFlow(getValue(key, defaultValue)).apply { - addListener(key, defaultValue) { - tryEmit(it) - } + addListener(key, defaultValue, ::tryEmit) +} + +private fun createNullableStateFlow( + key: String, + getValue: (String) -> T?, + addListener: (String, (T?) -> Unit) -> Unit, +): StateFlow = MutableStateFlow(getValue(key)).apply { + addListener(key, ::tryEmit) } fun ObservableSettings.boolAsFlow(key: String, defaultValue: Boolean) = @@ -20,3 +26,6 @@ fun ObservableSettings.boolAsFlow(key: String, defaultValue: Boolean) = fun ObservableSettings.stringAsFlow(key: String, defaultValue: String): StateFlow = createStateFlow(key, defaultValue, ::getString, ::addStringListener) + +fun ObservableSettings.nullableStringAsFlow(key: String): StateFlow = + createNullableStateFlow(key, ::getStringOrNull, ::addStringOrNullListener) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/SettingState.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/SettingState.kt index 562c7ed..d05a6eb 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/SettingState.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/utils/settings/SettingState.kt @@ -16,9 +16,23 @@ private fun createSettingState( defaultValue: T, createStateFlow: (String, T) -> StateFlow, updatePreference: (String, T) -> Unit, -) = SettingState(createStateFlow(key, defaultValue)) { - updatePreference(key, it) -} +) = SettingState( + delegateFlow = createStateFlow(key, defaultValue), + updatePreference = { + updatePreference(key, it) + }, +) + +private fun createNullableSettingState( + key: String, + createStateFlow: (String) -> StateFlow, + updatePreference: (String, T?) -> Unit, +) = SettingState( + delegateFlow = createStateFlow(key), + updatePreference = { + updatePreference(key, it) + }, +) fun ObservableSettings.boolSettingState(key: String, defaultValue: Boolean) = createSettingState(key, defaultValue, ::boolAsFlow, ::putBoolean) @@ -42,3 +56,22 @@ fun ObservableSettings.stringBackedSettingState( putString(key, toString(value)) }, ) + +fun ObservableSettings.nullableStringBackedSettingState( + key: String, + toString: (T) -> String = { it.toString() }, + fromString: (String) -> T, +): SettingState = createNullableSettingState( + key = key, + createStateFlow = { _ -> + nullableStringAsFlow(key) + .mapState { it?.let(fromString) } + }, + updatePreference = { _, value -> + if (value == null) { + remove(key) + } else { + putString(key, toString(value)) + } + }, +) diff --git a/shared/src/commonMain/resources/raw/forex.json b/shared/src/commonMain/resources/raw/forex.json new file mode 100644 index 0000000..f7e7a6a --- /dev/null +++ b/shared/src/commonMain/resources/raw/forex.json @@ -0,0 +1,1022 @@ +{ + "message": "Success", + "data": { + "timestamp": "2024-01-04T07:42:02Z", + "rates": [ + { + "currency": "AED", + "symbol": "د.إ.", + "name": "United Arab Emirates Dirham", + "rate": 3.6728027 + }, + { + "currency": "AFN", + "symbol": "؋", + "name": "Afghan Afghani", + "rate": 70.07695 + }, + { + "currency": "ALL", + "symbol": "L", + "name": "Albanian Lek", + "rate": 94.79759 + }, + { + "currency": "AMD", + "symbol": "դր", + "name": "Armenian Dram", + "rate": 404.5632 + }, + { + "currency": "ANG", + "symbol": "ƒ", + "name": "Netherlands Antillean Guilder", + "rate": 1.8025284 + }, + { + "currency": "AOA", + "symbol": "Kz", + "name": "Angolan Kwanza", + "rate": 832.49786 + }, + { + "currency": "ARS", + "symbol": "$", + "name": "Argentine Peso", + "rate": 810.66364 + }, + { + "currency": "AUD", + "symbol": "$", + "name": "Australian Dollar", + "rate": 1.4799807 + }, + { + "currency": "AWG", + "symbol": "ƒ", + "name": "Aruban Florin", + "rate": 1.8025001 + }, + { + "currency": "AZN", + "symbol": "₼", + "name": "Azerbaijani Manat", + "rate": 1.6954899 + }, + { + "currency": "BAM", + "symbol": "КМ", + "name": "Bosnia and Herzegovina Convertible Mark", + "rate": 1.791362 + }, + { + "currency": "BBD", + "symbol": "$", + "name": "Barbadian Dollar", + "rate": 2.019418 + }, + { + "currency": "BDT", + "symbol": "৳", + "name": "Bangladeshi Taka", + "rate": 109.76421 + }, + { + "currency": "BGN", + "symbol": "лв.", + "name": "Bulgarian Lev", + "rate": 1.7902449 + }, + { + "currency": "BHD", + "symbol": "د.ب.", + "name": "Bahraini Dinar", + "rate": 0.37691572 + }, + { + "currency": "BIF", + "symbol": "FBu", + "name": "Burundian Franc", + "rate": 2850.889 + }, + { + "currency": "BMD", + "symbol": "$", + "name": "Bermudian Dollar", + "rate": 1 + }, + { + "currency": "BND", + "symbol": "$", + "name": "Brunei Dollar", + "rate": 1.3285406 + }, + { + "currency": "BOB", + "symbol": "Bs.", + "name": "Bolivian Boliviano", + "rate": 6.9106393 + }, + { + "currency": "BRL", + "symbol": "R$", + "name": "Brazilian Real", + "rate": 4.9203997 + }, + { + "currency": "BSD", + "symbol": "$", + "name": "Bahamian Dollar", + "rate": 1.0000914 + }, + { + "currency": "BTC", + "symbol": "$", + "name": "BTC", + "rate": 0.000023374334 + }, + { + "currency": "BTN", + "symbol": "Nu.", + "name": "Bhutanese Ngultrum", + "rate": 83.58988 + }, + { + "currency": "BWP", + "symbol": "P", + "name": "Botswana Pula", + "rate": 13.570253 + }, + { + "currency": "BYN", + "symbol": "руб.", + "name": "Belarusian Ruble", + "rate": 3.3025284 + }, + { + "currency": "BYR", + "symbol": "$", + "name": "BYR", + "rate": 19600.004 + }, + { + "currency": "BZD", + "symbol": "$", + "name": "Belize Dollar", + "rate": 2.015937 + }, + { + "currency": "CAD", + "symbol": "$", + "name": "Canadian Dollar", + "rate": 1.3322654 + }, + { + "currency": "CDF", + "symbol": "₣", + "name": "Congolese Franc", + "rate": 2680.0002 + }, + { + "currency": "CHF", + "symbol": "₣", + "name": "Swiss Franc", + "rate": 0.8482107 + }, + { + "currency": "CLF", + "symbol": "$", + "name": "CLF", + "rate": 0.03197287 + }, + { + "currency": "CLP", + "symbol": "$", + "name": "Chilean Peso", + "rate": 882.23047 + }, + { + "currency": "CNY", + "symbol": "¥元", + "name": "Chinese Yuan", + "rate": 7.1029058 + }, + { + "currency": "COP", + "symbol": "$", + "name": "Colombian Peso", + "rate": 3898.501 + }, + { + "currency": "CRC", + "symbol": "₡", + "name": "Costa Rican Colon", + "rate": 519.6923 + }, + { + "currency": "CUC", + "symbol": "$", + "name": "Cuban convertible Peso", + "rate": 1 + }, + { + "currency": "CUP", + "symbol": "₱", + "name": "Cuban Peso", + "rate": 26.500006 + }, + { + "currency": "CVE", + "symbol": "$", + "name": "Cabo Verdean Escudo", + "rate": 100.994255 + }, + { + "currency": "CZK", + "symbol": "Kč", + "name": "Czech Koruna", + "rate": 22.541 + }, + { + "currency": "DJF", + "symbol": "ف.ج.", + "name": "Djiboutian Franc", + "rate": 178.07294 + }, + { + "currency": "DKK", + "symbol": "kr.", + "name": "Danish Krone", + "rate": 6.8172216 + }, + { + "currency": "DOP", + "symbol": "$", + "name": "Dominican Peso", + "rate": 58.041775 + }, + { + "currency": "DZD", + "symbol": "د.ج.", + "name": "Algerian Dinar", + "rate": 134.70186 + }, + { + "currency": "EGP", + "symbol": "ج.م.", + "name": "Egyptian Pound", + "rate": 30.898605 + }, + { + "currency": "ERN", + "symbol": "ناكفا", + "name": "Eritrean Nakfa", + "rate": 15.000001 + }, + { + "currency": "ETB", + "symbol": "ብር", + "name": "Ethiopian Birr", + "rate": 56.0306 + }, + { + "currency": "EUR", + "symbol": "€", + "name": "Euro", + "rate": 0.91408515 + }, + { + "currency": "FJD", + "symbol": "$", + "name": "Fijian Dollar", + "rate": 2.2342005 + }, + { + "currency": "FKP", + "symbol": "£", + "name": "Falkland Islands Pound", + "rate": 0.7911444 + }, + { + "currency": "GBP", + "symbol": "£", + "name": "Pound Sterling", + "rate": 0.78805023 + }, + { + "currency": "GEL", + "symbol": "₾", + "name": "Georgian Lari", + "rate": 2.6805081 + }, + { + "currency": "GGP", + "symbol": "£", + "name": "Guernsey Pound", + "rate": 0.7911444 + }, + { + "currency": "GHS", + "symbol": "₵", + "name": "Ghanaian Cedi", + "rate": 11.9518795 + }, + { + "currency": "GIP", + "symbol": "£", + "name": "Gibraltar Pound", + "rate": 0.7911444 + }, + { + "currency": "GMD", + "symbol": "D", + "name": "Gambian Dalasi", + "rate": 67.22499 + }, + { + "currency": "GNF", + "symbol": "FG", + "name": "Guinean Franc", + "rate": 8599.562 + }, + { + "currency": "GTQ", + "symbol": "$", + "name": "Guatemalan Quetzal", + "rate": 7.8238144 + }, + { + "currency": "GYD", + "symbol": "$", + "name": "Guyanese Dollar", + "rate": 209.40749 + }, + { + "currency": "HKD", + "symbol": "$", + "name": "Hong Kong Dollar", + "rate": 7.8085713 + }, + { + "currency": "HNL", + "symbol": "L", + "name": "Honduran Lempira", + "rate": 24.673937 + }, + { + "currency": "HRK", + "symbol": "kn", + "name": "Croatian Kuna", + "rate": 6.9817715 + }, + { + "currency": "HTG", + "symbol": "G", + "name": "Haitian Gourde", + "rate": 131.88315 + }, + { + "currency": "HUF", + "symbol": "Ft", + "name": "Hungarian Forint", + "rate": 347.22028 + }, + { + "currency": "IDR", + "symbol": "Rp", + "name": "Indonesian Rupiah", + "rate": 15497.604 + }, + { + "currency": "ILS", + "symbol": "₪", + "name": "Israeli new Shekel", + "rate": 3.652781 + }, + { + "currency": "IMP", + "symbol": "£", + "name": "Manx Pound", + "rate": 0.7911444 + }, + { + "currency": "INR", + "symbol": "₹", + "name": "Indian Rupee", + "rate": 83.24692 + }, + { + "currency": "IQD", + "symbol": "د.ع.", + "name": "Iraqi Dinar", + "rate": 1310.2279 + }, + { + "currency": "IRR", + "symbol": "﷼", + "name": "Iranian Rial", + "rate": 42060.008 + }, + { + "currency": "ISK", + "symbol": "kr", + "name": "Icelandic Krona", + "rate": 137.73984 + }, + { + "currency": "JEP", + "symbol": "£", + "name": "Jersey Pound", + "rate": 0.7911444 + }, + { + "currency": "JMD", + "symbol": "$", + "name": "Jamaican Dollar", + "rate": 154.33232 + }, + { + "currency": "JOD", + "symbol": "د.أ.", + "name": "Jordanian Dinar", + "rate": 0.7094992 + }, + { + "currency": "JPY", + "symbol": "¥", + "name": "Japanese Yen", + "rate": 143.26852 + }, + { + "currency": "KES", + "symbol": "KSh", + "name": "Kenyan Shilling", + "rate": 157.49966 + }, + { + "currency": "KGS", + "symbol": "с", + "name": "Kyrgyzstani Som", + "rate": 89.08532 + }, + { + "currency": "KHR", + "symbol": "៛", + "name": "Cambodian Riel", + "rate": 4083.1663 + }, + { + "currency": "KMF", + "symbol": "CF", + "name": "Comorian Franc", + "rate": 450.49127 + }, + { + "currency": "KPW", + "symbol": "₩", + "name": "North Korean Won", + "rate": 899.9596 + }, + { + "currency": "KRW", + "symbol": "₩", + "name": "South Korean Won", + "rate": 1306.1205 + }, + { + "currency": "KWD", + "symbol": "د.ك.", + "name": "Kuwaiti Dinar", + "rate": 0.30740958 + }, + { + "currency": "KYD", + "symbol": "$", + "name": "Cayman Islands Dollar", + "rate": 0.8334903 + }, + { + "currency": "KZT", + "symbol": "₸", + "name": "Kazakhstani Tenge", + "rate": 457.63177 + }, + { + "currency": "LAK", + "symbol": "₭", + "name": "Lao Kip", + "rate": 20612.848 + }, + { + "currency": "LBP", + "symbol": "ل.ل.", + "name": "Lebanese Pound", + "rate": 15032.06 + }, + { + "currency": "LKR", + "symbol": "රු or ரூ", + "name": "Sri Lankan Rupee", + "rate": 323.06436 + }, + { + "currency": "LRD", + "symbol": "$", + "name": "Liberian Dollar", + "rate": 188.49327 + }, + { + "currency": "LSL", + "symbol": "L", + "name": "Lesotho Loti", + "rate": 18.570108 + }, + { + "currency": "LTL", + "symbol": "$", + "name": "LTL", + "rate": 2.952741 + }, + { + "currency": "LVL", + "symbol": "$", + "name": "LVL", + "rate": 0.60489035 + }, + { + "currency": "LYD", + "symbol": "ل.د.", + "name": "Libyan Dinar", + "rate": 4.7957506 + }, + { + "currency": "MAD", + "symbol": "د.م.", + "name": "Moroccan Dirham", + "rate": 9.93117 + }, + { + "currency": "MDL", + "symbol": "L", + "name": "Moldovan Leu", + "rate": 17.572819 + }, + { + "currency": "MGA", + "symbol": "Ar", + "name": "Malagasy Ariary", + "rate": 4607.988 + }, + { + "currency": "MKD", + "symbol": "ден", + "name": "Macedonian Denar", + "rate": 56.347584 + }, + { + "currency": "MMK", + "symbol": "Ks", + "name": "Myanmar Kyat", + "rate": 2100.2935 + }, + { + "currency": "MNT", + "symbol": "₮", + "name": "Mongolian Tögrög", + "rate": 3421.8403 + }, + { + "currency": "MOP", + "symbol": "MOP$", + "name": "Macanese Pataca", + "rate": 8.044277 + }, + { + "currency": "MRU", + "symbol": "أ.م.", + "name": "Mauritanian Ouguiya", + "rate": 39.64979 + }, + { + "currency": "MUR", + "symbol": "रु ", + "name": "Mauritian Rupee", + "rate": 44.750496 + }, + { + "currency": "MVR", + "symbol": ".ރ", + "name": "Maldivian Rufiyaa", + "rate": 15.394975 + }, + { + "currency": "MWK", + "symbol": "MK", + "name": "Malawian Kwacha", + "rate": 1683.642 + }, + { + "currency": "MXN", + "symbol": "$", + "name": "Mexican Peso", + "rate": 17.011995 + }, + { + "currency": "MYR", + "symbol": "RM", + "name": "Malaysian Ringgit", + "rate": 4.6369567 + }, + { + "currency": "MZN", + "symbol": "MT", + "name": "Mozambican Metical", + "rate": 63.24996 + }, + { + "currency": "NAD", + "symbol": "$", + "name": "Namibian Dollar", + "rate": 18.57005 + }, + { + "currency": "NGN", + "symbol": "₦", + "name": "Nigerian Naira", + "rate": 891.90356 + }, + { + "currency": "NIO", + "symbol": "C$", + "name": "Nicaraguan Córdoba", + "rate": 36.609276 + }, + { + "currency": "NOK", + "symbol": "kr", + "name": "Norwegian Krone", + "rate": 10.304652 + }, + { + "currency": "NPR", + "symbol": "रू", + "name": "Nepalese Rupee", + "rate": 133.74338 + }, + { + "currency": "NZD", + "symbol": "$", + "name": "New Zealand Dollar", + "rate": 1.5928701 + }, + { + "currency": "OMR", + "symbol": "ر.ع.", + "name": "Omani Rial", + "rate": 0.38493863 + }, + { + "currency": "PAB", + "symbol": "B/.", + "name": "Panamanian Balboa", + "rate": 1.0001884 + }, + { + "currency": "PEN", + "symbol": "S/.", + "name": "Peruvian Sol", + "rate": 3.7045422 + }, + { + "currency": "PGK", + "symbol": "K", + "name": "Papua New Guinean Kina", + "rate": 3.7314527 + }, + { + "currency": "PHP", + "symbol": "₱", + "name": "Philippine Peso", + "rate": 55.499012 + }, + { + "currency": "PKR", + "symbol": "Rs", + "name": "Pakistani Rupee", + "rate": 282.0388 + }, + { + "currency": "PLN", + "symbol": "zł", + "name": "Polish Zloty", + "rate": 3.9756505 + }, + { + "currency": "PYG", + "symbol": "₲", + "name": "Paraguayan Guaraní", + "rate": 7274.227 + }, + { + "currency": "QAR", + "symbol": "ر.ق.", + "name": "Qatari Riyal", + "rate": 3.6404986 + }, + { + "currency": "RON", + "symbol": "L", + "name": "Romanian Leu", + "rate": 4.5450993 + }, + { + "currency": "RSD", + "symbol": "дин", + "name": "Serbian Dinar", + "rate": 107.15303 + }, + { + "currency": "RUB", + "symbol": "₽", + "name": "Russian Ruble", + "rate": 91.31941 + }, + { + "currency": "RWF", + "symbol": "R₣", + "name": "Rwandan Franc", + "rate": 1260.6708 + }, + { + "currency": "SAR", + "symbol": "ر.س.", + "name": "Saudi Riyal", + "rate": 3.750441 + }, + { + "currency": "SBD", + "symbol": "$", + "name": "Solomon Islands Dollar", + "rate": 8.404699 + }, + { + "currency": "SCR", + "symbol": "Rs", + "name": "Seychellois Rupee", + "rate": 13.323097 + }, + { + "currency": "SDG", + "symbol": "ج.س.", + "name": "Sudanese Pound", + "rate": 601.0004 + }, + { + "currency": "SEK", + "symbol": "kr", + "name": "Swedish Krona", + "rate": 10.237157 + }, + { + "currency": "SGD", + "symbol": "$", + "name": "Singapore Dollar", + "rate": 1.3275204 + }, + { + "currency": "SHP", + "symbol": "£", + "name": "Saint Helena Pound", + "rate": 1.2623013 + }, + { + "currency": "SLE", + "symbol": "$", + "name": "SLE", + "rate": 22.579914 + }, + { + "currency": "SLL", + "symbol": "Le", + "name": "Sierra Leonean Leone", + "rate": 19750.004 + }, + { + "currency": "SOS", + "symbol": "Ssh", + "name": "Somali Shilling", + "rate": 571.0002 + }, + { + "currency": "SRD", + "symbol": "$", + "name": "Surinamese Dollar", + "rate": 36.399506 + }, + { + "currency": "STD", + "symbol": "$", + "name": "STD", + "rate": 20697.984 + }, + { + "currency": "SYP", + "symbol": "ل.س.", + "name": "Syrian Pound", + "rate": 13001.785 + }, + { + "currency": "SZL", + "symbol": "L", + "name": "Swazi Lilangeni", + "rate": 18.791906 + }, + { + "currency": "THB", + "symbol": "฿", + "name": "Thai Baht", + "rate": 34.468506 + }, + { + "currency": "TJS", + "symbol": "SM", + "name": "Tajikistani Somoni", + "rate": 10.926596 + }, + { + "currency": "TMT", + "symbol": "T", + "name": "Turkmenistan Manat", + "rate": 3.5100002 + }, + { + "currency": "TND", + "symbol": "د.ت.", + "name": "Tunisian Dinar", + "rate": 3.0934973 + }, + { + "currency": "TOP", + "symbol": "PT", + "name": "Tongan Paʻanga", + "rate": 2.3431504 + }, + { + "currency": "TRY", + "symbol": "₺", + "name": "Turkish Lira", + "rate": 29.780073 + }, + { + "currency": "TTD", + "symbol": "$", + "name": "Trinidad and Tobago Dollar", + "rate": 6.7879057 + }, + { + "currency": "TWD", + "symbol": "圓", + "name": "New Taiwan Dollar", + "rate": 30.977505 + }, + { + "currency": "TZS", + "symbol": "TSh", + "name": "Tanzanian Shilling", + "rate": 2515 + }, + { + "currency": "UAH", + "symbol": "грн", + "name": "Ukrainian Hryvnia", + "rate": 38.124023 + }, + { + "currency": "UGX", + "symbol": "Sh", + "name": "Ugandan Shilling", + "rate": 3812.9875 + }, + { + "currency": "USD", + "symbol": "$", + "name": "United States Dollar", + "rate": 1 + }, + { + "currency": "UYU", + "symbol": "$", + "name": "Uruguayan Peso", + "rate": 39.03664 + }, + { + "currency": "UZS", + "symbol": "сум", + "name": "Uzbekistani Som", + "rate": 12340.23 + }, + { + "currency": "VEF", + "symbol": "$", + "name": "VEF", + "rate": 3582025.5 + }, + { + "currency": "VES", + "symbol": "Bs.F", + "name": "Venezuelan Bolívar Soberano", + "rate": 35.8222 + }, + { + "currency": "VND", + "symbol": "₫", + "name": "Vietnamese Dong", + "rate": 24390.004 + }, + { + "currency": "VUV", + "symbol": "VT", + "name": "Vanuatu Vatu", + "rate": 118.78398 + }, + { + "currency": "WST", + "symbol": "ST", + "name": "Samoan Tala", + "rate": 2.7146337 + }, + { + "currency": "XAF", + "symbol": "Fr.", + "name": "Central African CFA Franc BEAC", + "rate": 600.8034 + }, + { + "currency": "XAG", + "symbol": "$", + "name": "XAG", + "rate": 0.043419044 + }, + { + "currency": "XAU", + "symbol": "$", + "name": "XAU", + "rate": 0.00048812147 + }, + { + "currency": "XCD", + "symbol": "$", + "name": "East Caribbean Dollar", + "rate": 2.7025504 + }, + { + "currency": "XDR", + "symbol": "$", + "name": "XDR", + "rate": 0.7454666 + }, + { + "currency": "XOF", + "symbol": "₣", + "name": "West African CFA Franc BCEAO", + "rate": 600.8034 + }, + { + "currency": "XPF", + "symbol": "₣", + "name": "CFP Franc (Franc Pacifique)", + "rate": 109.07938 + }, + { + "currency": "YER", + "symbol": "ر.ي.", + "name": "Yemeni Rial", + "rate": 250.25012 + }, + { + "currency": "ZAR", + "symbol": "R", + "name": "South African Rand", + "rate": 18.648893 + }, + { + "currency": "ZMK", + "symbol": "$", + "name": "ZMK", + "rate": 9001.194 + }, + { + "currency": "ZMW", + "symbol": "ZK", + "name": "Zambian Kwacha", + "rate": 25.778536 + }, + { + "currency": "ZWL", + "symbol": "$", + "name": "Zimbabwean Dollar", + "rate": 321.99963 + } + ] + } +} diff --git a/shared/src/commonMain/sqldelight/me/sujanpoudel/playdeals/common/Forex.sq b/shared/src/commonMain/sqldelight/me/sujanpoudel/playdeals/common/Forex.sq new file mode 100644 index 0000000..1bdb40a --- /dev/null +++ b/shared/src/commonMain/sqldelight/me/sujanpoudel/playdeals/common/Forex.sq @@ -0,0 +1,13 @@ +upsert: + INSERT OR REPLACE INTO ForexRate ( + currency, + symbol, + name, + rate + ) VALUES (?,?,?,?); + +getAll: + SELECT * FROM ForexRate ORDER BY name DESC; + +removeStale: + DELETE FROM ForexRate WHERE currency NOT IN ?; diff --git a/shared/src/commonMain/sqldelight/migrations/1.sqm b/shared/src/commonMain/sqldelight/migrations/1.sqm new file mode 100644 index 0000000..64fe0af --- /dev/null +++ b/shared/src/commonMain/sqldelight/migrations/1.sqm @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS ForexRate +( + currency TEXT NOT NULL PRIMARY KEY, + symbol TEXT NOT NULL, + name TEXT NOT NULL, + rate REAL NOT NULL +);