Skip to content

Commit

Permalink
Merge pull request #65 from psuzn/multi-currency
Browse files Browse the repository at this point in the history
multi currency support
  • Loading branch information
psuzn authored Jan 5, 2024
2 parents 5cb5a01 + e313b74 commit 2021ede
Show file tree
Hide file tree
Showing 29 changed files with 1,535 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .run/iosApp.run.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="iosApp" type="KmmRunConfiguration" factoryName="iOS Application" CONFIG_VERSION="1" EXEC_TARGET_ID="E03FCF7C-99D3-4C80-8793-0DE2C2CD6295" XCODE_PROJECT="$PROJECT_DIR$/iosApp/iosApp.xcworkspace" XCODE_CONFIGURATION="Debug" XCODE_SCHEME="iosApp">
<configuration default="false" name="iosApp" type="KmmRunConfiguration" factoryName="iOS Application" CONFIG_VERSION="1" EXEC_TARGET_ID="65CD8F7A-DF08-4A83-8101-98F41069F974" XCODE_PROJECT="$PROJECT_DIR$/iosApp/iosApp.xcworkspace" XCODE_CONFIGURATION="Debug" XCODE_SCHEME="iosApp">
<method v="2">
<option name="com.jetbrains.kmm.ios.BuildIOSAppTask" enabled="true" />
</method>
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,22 +21,50 @@ 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
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 {
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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()

Expand All @@ -125,16 +147,12 @@ class HomeScreenViewModelTest {
runTest {
val dispatcher = StandardTestDispatcher()

val deal = mockk<DealEntity>()

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()

Expand All @@ -151,15 +169,14 @@ class HomeScreenViewModelTest {
runTest {
val dispatcher = StandardTestDispatcher()

val entity = mockk<DealEntity>()
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()

Expand All @@ -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
Expand Down Expand Up @@ -230,9 +247,7 @@ class HomeScreenViewModelTest {
runTest {
val dispatcher = StandardTestDispatcher()

val entity = mockk<DealEntity>()

every { entity.category } returns ""
val entity = dealEntity("1")

setMainDispatcher(dispatcher)

Expand All @@ -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
Expand Down Expand Up @@ -289,20 +304,16 @@ class HomeScreenViewModelTest {
val dispatcher = StandardTestDispatcher()
setMainDispatcher(dispatcher)

val deal1 = mockk<DealEntity>()
val deal2 = mockk<DealEntity>()
val deal1 = dealEntity("123")
val deal2 = dealEntity("321")

val flow = MutableStateFlow<List<DealEntity>>(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 ->
Expand All @@ -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)
Expand All @@ -324,18 +336,15 @@ class HomeScreenViewModelTest {
val dispatcher = StandardTestDispatcher()
setMainDispatcher(dispatcher)

val deal1 = mockk<DealEntity>()
val deal2 = mockk<DealEntity>()
val deal1 = dealEntity()
val deal2 = dealEntity("2")

val flow = MutableStateFlow<List<DealEntity>>(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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,4 +31,11 @@ internal val persistentModule = DI.Module("persistent") {
appPreferences = instance(),
)
}
bindSingleton {
ForexRepository(
remoteAPI = instance(),
appPreferences = instance(),
database = instance(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ForexRateEntity>,
)

@Immutable
data class ForexRateEntity(
val currency: String,
val symbol: String,
val name: String,
val rate: Float,
)
Original file line number Diff line number Diff line change
@@ -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<ForexRate>,
)

@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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -13,4 +14,6 @@ class RemoteAPI(
deal.toEntity()
}
}

suspend fun getForexRates() = client.get<ForexModel>("/forex")
}
Loading

0 comments on commit 2021ede

Please sign in to comment.