Skip to content

Commit 2021ede

Browse files
authored
Merge pull request #65 from psuzn/multi-currency
multi currency support
2 parents 5cb5a01 + e313b74 commit 2021ede

File tree

29 files changed

+1535
-118
lines changed

29 files changed

+1535
-118
lines changed

.run/iosApp.run.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<component name="ProjectRunConfigurationManager">
2-
<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">
2+
<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">
33
<method v="2">
44
<option name="com.jetbrains.kmm.ios.BuildIOSAppTask" enabled="true" />
55
</method>

gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sqldelight = "2.0.0"
1111
google-services = "4.4.0"
1212

1313
# Libraries
14-
compose-compiler = "1.5.6"
14+
compose-compiler = "1.5.7"
1515
accompanist-permissions = "0.32.0"
1616
activity-compose = "1.8.2"
1717
appcompat = "1.6.1"

shared/src/androidUnitTest/kotlin/me/sujanpoudel/playdeals/common/ui/screens/HomeScreenViewModelTest.kt

+45-36
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import io.kotest.matchers.shouldBe
88
import io.mockk.MockKAnnotations
99
import io.mockk.coEvery
1010
import io.mockk.coVerify
11-
import io.mockk.every
1211
import io.mockk.impl.annotations.MockK
13-
import io.mockk.mockk
1412
import kotlinx.coroutines.ExperimentalCoroutinesApi
1513
import kotlinx.coroutines.flow.MutableStateFlow
1614
import kotlinx.coroutines.flow.emptyFlow
@@ -23,22 +21,50 @@ import me.sujanpoudel.playdeals.common.domain.models.Failure
2321
import me.sujanpoudel.playdeals.common.domain.models.Result
2422
import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences
2523
import me.sujanpoudel.playdeals.common.domain.repositories.DealsRepository
24+
import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository
25+
import me.sujanpoudel.playdeals.common.domain.repositories.defaultUsdConversion
2626
import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenState
2727
import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenViewModel
2828
import me.sujanpoudel.playdeals.common.utils.setMainDispatcher
2929
import me.sujanpoudel.playdeals.common.utils.settings.asObservableSettings
3030
import org.junit.jupiter.api.BeforeEach
3131
import org.junit.jupiter.api.Test
3232

33+
fun dealEntity(id: String = "1") = DealEntity(
34+
id = id,
35+
name = "name",
36+
icon = "icon",
37+
images = listOf(),
38+
normalPrice = 1.1f,
39+
currentPrice = 1.2f,
40+
currency = "$",
41+
url = "",
42+
category = "game",
43+
downloads = "150k",
44+
rating = 3.7f,
45+
offerExpiresIn = kotlinx.datetime.Clock.System.now(),
46+
type = "",
47+
source = "",
48+
createdAt = kotlinx.datetime.Clock.System.now(),
49+
updatedAt = kotlinx.datetime.Clock.System.now(),
50+
)
51+
3352
@OptIn(ExperimentalCoroutinesApi::class)
3453
class HomeScreenViewModelTest {
3554

3655
@MockK
3756
lateinit var dealsRepository: DealsRepository
3857

58+
@MockK
59+
lateinit var forexRepository: ForexRepository
60+
3961
@BeforeEach
4062
fun setup() {
4163
MockKAnnotations.init(this)
64+
coEvery { forexRepository.refreshRatesIfNecessary() } returns Result.success(Unit)
65+
coEvery { forexRepository.forexRatesFlow() } returns flowOf(emptyList())
66+
coEvery { forexRepository.forexUpdateAtFlow() } returns MutableStateFlow(null)
67+
coEvery { forexRepository.preferredConversionRateFlow() } returns MutableStateFlow(defaultUsdConversion())
4268
}
4369

4470
private fun appPreference(): AppPreferences {
@@ -62,7 +88,7 @@ class HomeScreenViewModelTest {
6288
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
6389
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
6490

65-
HomeScreenViewModel(appPreference(), dealsRepository)
91+
HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
6692

6793
coVerify {
6894
dealsRepository.dealsFlow()
@@ -76,21 +102,17 @@ class HomeScreenViewModelTest {
76102
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
77103
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
78104

79-
HomeScreenViewModel(appPreference(), dealsRepository)
105+
HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
80106

81107
coVerify { dealsRepository.refreshDeals() }
82108
}
83109

84110
@Test
85111
fun `should correctly update state before calling the remoteAPI_getDeals`(): Unit = runTest {
86-
val dispatcher = StandardTestDispatcher()
87-
88-
setMainDispatcher(dispatcher)
89-
90112
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
91113
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
92114

93-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
115+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
94116

95117
viewModel.state.value.let {
96118
it.allDeals shouldHaveSize 0
@@ -108,7 +130,7 @@ class HomeScreenViewModelTest {
108130
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
109131
coEvery { dealsRepository.refreshDeals() } returns Result.failure(Failure.UnknownError)
110132

111-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
133+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
112134

113135
dispatcher.scheduler.runCurrent()
114136

@@ -125,16 +147,12 @@ class HomeScreenViewModelTest {
125147
runTest {
126148
val dispatcher = StandardTestDispatcher()
127149

128-
val deal = mockk<DealEntity>()
129-
130-
every { deal.category } returns ""
131-
132150
setMainDispatcher(dispatcher)
133151

134152
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
135153
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
136154

137-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
155+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
138156

139157
dispatcher.scheduler.runCurrent()
140158

@@ -151,15 +169,14 @@ class HomeScreenViewModelTest {
151169
runTest {
152170
val dispatcher = StandardTestDispatcher()
153171

154-
val entity = mockk<DealEntity>()
155-
every { entity.category } returns ""
172+
val entity = dealEntity("32")
156173

157174
setMainDispatcher(dispatcher)
158175

159176
coEvery { dealsRepository.dealsFlow() } returns flowOf(listOf(entity))
160177
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
161178

162-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
179+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
163180

164181
dispatcher.scheduler.runCurrent()
165182

@@ -184,7 +201,7 @@ class HomeScreenViewModelTest {
184201

185202
coEvery { dealsRepository.dealsFlow() } returns emptyFlow()
186203

187-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
204+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
188205

189206
viewModel.state.value.also { state ->
190207
state.allDeals shouldHaveSize 0
@@ -230,9 +247,7 @@ class HomeScreenViewModelTest {
230247
runTest {
231248
val dispatcher = StandardTestDispatcher()
232249

233-
val entity = mockk<DealEntity>()
234-
235-
every { entity.category } returns ""
250+
val entity = dealEntity("1")
236251

237252
setMainDispatcher(dispatcher)
238253

@@ -242,7 +257,7 @@ class HomeScreenViewModelTest {
242257

243258
coEvery { dealsRepository.dealsFlow() } returns flowOf(listOf(entity))
244259

245-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
260+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
246261

247262
viewModel.state.value.also { state ->
248263
state.allDeals shouldHaveSize 0
@@ -289,20 +304,16 @@ class HomeScreenViewModelTest {
289304
val dispatcher = StandardTestDispatcher()
290305
setMainDispatcher(dispatcher)
291306

292-
val deal1 = mockk<DealEntity>()
293-
val deal2 = mockk<DealEntity>()
307+
val deal1 = dealEntity("123")
308+
val deal2 = dealEntity("321")
294309

295310
val flow = MutableStateFlow<List<DealEntity>>(emptyList())
296311

297-
every { deal1.category } returns ""
298-
every { deal2.category } returns ""
299-
300312
coEvery { dealsRepository.refreshDeals() } returns Result.success(Unit)
301313
coEvery { dealsRepository.dealsFlow() } returns flow
302314

303-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
315+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
304316

305-
dispatcher.scheduler.advanceTimeBy(10000)
306317
dispatcher.scheduler.advanceUntilIdle()
307318

308319
viewModel.state.value.also { state ->
@@ -312,6 +323,7 @@ class HomeScreenViewModelTest {
312323
flow.emit(listOf(deal1, deal2))
313324

314325
dispatcher.scheduler.advanceUntilIdle()
326+
dispatcher.scheduler.runCurrent()
315327

316328
viewModel.state.value.also { state ->
317329
state.allDeals.shouldContainExactly(deal1, deal2)
@@ -324,18 +336,15 @@ class HomeScreenViewModelTest {
324336
val dispatcher = StandardTestDispatcher()
325337
setMainDispatcher(dispatcher)
326338

327-
val deal1 = mockk<DealEntity>()
328-
val deal2 = mockk<DealEntity>()
339+
val deal1 = dealEntity()
340+
val deal2 = dealEntity("2")
329341

330342
val flow = MutableStateFlow<List<DealEntity>>(emptyList())
331343

332-
every { deal1.category } returns ""
333-
every { deal2.category } returns ""
334-
335344
coEvery { dealsRepository.refreshDeals() } returns Result.failure(Failure.UnknownError)
336345
coEvery { dealsRepository.dealsFlow() } returns flow
337346

338-
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository)
347+
val viewModel = HomeScreenViewModel(appPreference(), dealsRepository, forexRepository)
339348

340349
dispatcher.scheduler.advanceTimeBy(10000)
341350
dispatcher.scheduler.advanceUntilIdle()
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package me.sujanpoudel.playdeals.common
22

3+
import kotlin.time.Duration.Companion.hours
4+
35
object Constants {
46
const val API_BASE_URL = "https://api.play-deals.contabo.sujanpoudel.me/api"
57
const val ABOUT_ME_URL = "https://sujanpoudel.me"
68
val DATABASE_NAME = "${BuildKonfig.PACKAGE_NAME}.db"
9+
10+
val FOREX_REFRESH_DURATION = 5.hours
11+
const val BUNDLED_FOREX = "raw/forex.json"
712
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/persistent.kt

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences
66
import me.sujanpoudel.playdeals.common.domain.persistent.db.buildDealAdaptor
77
import me.sujanpoudel.playdeals.common.domain.persistent.db.createSqlDriver
88
import me.sujanpoudel.playdeals.common.domain.repositories.DealsRepository
9+
import me.sujanpoudel.playdeals.common.domain.repositories.ForexRepository
910
import me.sujanpoudel.playdeals.common.utils.settings.asObservableSettings
1011
import org.kodein.di.DI
1112
import org.kodein.di.bindSingleton
@@ -30,4 +31,11 @@ internal val persistentModule = DI.Module("persistent") {
3031
appPreferences = instance(),
3132
)
3233
}
34+
bindSingleton {
35+
ForexRepository(
36+
remoteAPI = instance(),
37+
appPreferences = instance(),
38+
database = instance(),
39+
)
40+
}
3341
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ internal val viewModelModule = DI.Module("viewModel") {
1313
bindProvider {
1414
HomeScreenViewModel(
1515
appPreferences = instance(),
16-
repository = instance(),
16+
dealsRepository = instance(),
17+
forexRepository = instance(),
1718
)
1819
}
1920

2021
bindProvider { NewDealScreenViewModel() }
2122

22-
bindProvider { SettingsScreenViewModel(instance()) }
23+
bindProvider { SettingsScreenViewModel(instance(), instance()) }
2324

2425
bindProvider { NotificationSettingsScreenViewModel(instance()) }
2526
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/entities/DealEntity.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable
55
import kotlinx.datetime.Clock
66
import kotlinx.datetime.Instant
77
import me.sujanpoudel.playdeals.common.strings.Strings
8-
import me.sujanpoudel.playdeals.common.utils.asCurrencySymbol
98
import me.sujanpoudel.playdeals.common.utils.formatAsPrice
109
import me.sujanpoudel.playdeals.common.utils.shallowFormatted
1110

@@ -31,14 +30,14 @@ data class DealEntity(
3130
val ratingFormatted: String
3231
get() = rating.formatAsPrice()
3332

34-
fun formattedNormalPrice() = "${currency.asCurrencySymbol()}${normalPrice.formatAsPrice()}"
33+
fun formattedNormalPrice() = "${currency}${normalPrice.formatAsPrice()}"
3534

3635
@Composable
3736
fun formattedCurrentPrice() =
3837
if (currentPrice == 0f) {
3938
Strings.free
4039
} else {
41-
"${currency.asCurrencySymbol()}${currentPrice.formatAsPrice()}"
40+
"${currency}${currentPrice.formatAsPrice()}"
4241
}
4342

4443
@Composable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package me.sujanpoudel.playdeals.common.domain.entities
2+
3+
import androidx.compose.runtime.Immutable
4+
import kotlinx.datetime.Instant
5+
6+
@Immutable
7+
data class Forex(
8+
val timestamp: Instant,
9+
val rates: List<ForexRateEntity>,
10+
)
11+
12+
@Immutable
13+
data class ForexRateEntity(
14+
val currency: String,
15+
val symbol: String,
16+
val name: String,
17+
val rate: Float,
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package me.sujanpoudel.playdeals.common.domain.models.api
2+
3+
import androidx.compose.runtime.Immutable
4+
import kotlinx.datetime.Instant
5+
import kotlinx.serialization.Serializable
6+
import me.sujanpoudel.playdeals.common.domain.entities.ForexRateEntity
7+
8+
@Serializable
9+
@Immutable
10+
data class ForexModel(
11+
val timestamp: Instant,
12+
val rates: List<ForexRate>,
13+
)
14+
15+
@Serializable
16+
@Immutable
17+
data class ForexRate(
18+
val currency: String,
19+
val symbol: String,
20+
val name: String,
21+
val rate: Float,
22+
)
23+
24+
fun ForexRate.toEntity() = ForexRateEntity(
25+
currency = currency,
26+
symbol = symbol,
27+
name = name,
28+
rate = rate,
29+
)

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/networking/RemoteAPI.kt

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package me.sujanpoudel.playdeals.common.domain.networking
22

33
import io.ktor.client.HttpClient
44
import me.sujanpoudel.playdeals.common.domain.models.api.AppDealModel
5+
import me.sujanpoudel.playdeals.common.domain.models.api.ForexModel
56
import me.sujanpoudel.playdeals.common.domain.models.api.toEntity
67
import me.sujanpoudel.playdeals.common.domain.models.map
78

@@ -13,4 +14,6 @@ class RemoteAPI(
1314
deal.toEntity()
1415
}
1516
}
17+
18+
suspend fun getForexRates() = client.get<ForexModel>("/forex")
1619
}

0 commit comments

Comments
 (0)