From 694dcac46be416e51aefed36fbdfdc3758b3db70 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 24 Mar 2024 22:49:48 +0100 Subject: [PATCH] [Oztechan/CCC#2854] Remove experimental multiplatform settings (#3259) * Revert "[Oztechan/CCC#3253] Clean up Swift code (#3254)" This reverts commit 92ac1b356357980b8d49b5634eddf8f9fc20800f. * Revert "[Oztechan/CCC#3251] Prevent multiple Api call when base change on click and add test cases (#3252)" This reverts commit 6755d6996ba402687eaeafb96c626623e91fe0d8. * Revert "[Oztechan/CCC#3191] Add Flow Property for lastInput (#3192)" This reverts commit 375da22506004af9a1337254eef32a7c936cd8eb. * Revert "[Oztechan/CCC#3189] Move to new getBaseFlow logic (#3190)" This reverts commit 4e4efc96 * Revert "[Oztechan/CCC#3185] Remove navigation result logics (#3186)" This reverts commit ba2563542195f3cfe3216bba44e61623ae03cfcd. * Revert "[Oztechan/CCC#3183] Remove onBaseChange event (#3184)" This reverts commit 917be17cd2882fbec0370799814a25b3341a19cc. * Revert "[Oztechan/CCC#3180] Remove ChangeBase Effect (#3181)" This reverts commit 6fab011d587a844df04ad8957b266a6de092af5a. * Revert "[Oztechan/CCC#3178] Replace CurrencyChange effect with DismissDialog (#3179)" This reverts commit fc2f955348c0cfcc67c5a00d7a7b4c7b0108b5a7. * Revert "[Oztechan/CCC#3176] Add Flow Property for currentBase (#3177)" This reverts commit dd2bdc55988367fe38959cbb9daf64d609bd56db. * Revert "[Oztechan/CCC#3144] Remove Persistence and use DI dispatchers in Coroutine based Persistences (#3145)" This reverts commit c59e1a5c575377141ed7b27ba461d76ea7578a49. * Revert "[Oztechan/CCC#3141] Move sessionCount to Suspend (#3142)" This reverts commit e2eaa85b581c08bb8cb6756800ca8a791ae1a59e. * Revert "[Oztechan/CCC#3138] Move premiumEndDate to Suspend (#3140)" This reverts commit bd0f4b9c0c0cdc55b351050212d4927d585256c0. * Revert "[Oztechan/CCC#2942] Move firstRun to Suspend (#3137)" This reverts commit 2ff3edf759c6e9c4c2053cd25192cf753dcb1c73. * Revert "[Oztechan/CCC#3128] Move appTheme to Suspend (#3135)" This reverts commit 00c10be7379d3af24f7592f8bccbad4c40c2fd2e. * Revert "[Oztechan/CCC#3122] Move currentBase to Suspend (#3127)" This reverts commit de4de42f * Revert "[Oztechan/CCC#3123] Move precision to Suspend (#3124)" This reverts commit 26af2a18 * Revert "[Oztechan/CCC#3120] Move lastInput to Suspend (#3121)" This reverts commit 9c53434f533db4a904f1e20d5a749101963b81b0. * Revert "[Oztechan/CCC#3038] Add missing Settings Koin definition for native target (#3039)" This reverts commit 98870c38a3cf204bce2cadc40b8def491ba9d148. * Revert "[Oztechan/CCC#2871] Add error test case for FlowPersistence (#2872)" This reverts commit c1f8711b3f2bd36edb6574aebf8c315a63c6b4fc. * Revert "[Oztechan/CCC#2867] Create test for FlowPersistence (#2868)" This reverts commit 47ea4f95 * Revert "[Oztechan/CCC#2865] Create test for SuspendPersistence (#2866)" This reverts commit fdc952c58894b84d3b32e3bd50bb29e2fede40f0. * Revert "[Oztechan/CCC#2869] Opt-in for ExperimentalSettingsApi (#2870)" This reverts commit ef9f1877746c949f279fce027d48ab657136d728. * Revert "[Oztechan/CCC#2863] Use relevant settings for persistencs (#2864)" This reverts commit 00696db7 * Revert "[Oztechan/CCC#2857] Add Suspension support for multiplatform settings (#2859)" This reverts commit 5eaa8b15 * Revert "[Oztechan/CCC#2855] Add Flow support for multiplatform settings (#2856)" This reverts commit 9b529096 * [Oztechan/CCC#2854] Remove experimental multiplatform settings * [Oztechan/CCC#2854] Remove experimental multiplatform settings * [Oztechan/CCC#2854] Remove experimental multiplatform settings --- .../content/calculator/CalculatorFragment.kt | 14 + .../content/currencies/CurrenciesFragment.kt | 8 + .../ui/mobile/content/main/MainActivity.kt | 13 +- .../SelectCurrencyBottomSheet.kt | 11 +- .../android/ui/mobile/util/ViewExtensions.kt | 21 + .../android/viewmodel/widget/WidgetSEED.kt | 4 +- .../viewmodel/widget/WidgetViewModel.kt | 38 +- .../viewmodel/widget/WidgetViewModelTest.kt | 59 ++- .../client-core-persistence.gradle.kts | 12 - .../di/ClientCorePersistenceModule.android.kt | 24 +- .../core/persistence/FlowPersistence.kt | 7 - .../core/persistence/FlowPersistenceImpl.kt | 19 - .../client/core/persistence/Persistence.kt | 6 + .../core/persistence/PersistenceImpl.kt | 25 ++ .../core/persistence/SuspendPersistence.kt | 6 - .../persistence/SuspendPersistenceImpl.kt | 29 -- .../core/persistence/FlowPersistenceTest.kt | 75 ---- .../core/persistence/PersistenceTest.kt | 91 ++++ .../persistence/SuspendPersistenceTest.kt | 96 ----- .../di/ClientCorePersistenceModule.ios.kt | 26 +- .../client-repository-adcontrol.gradle.kts | 1 - .../adcontrol/AdControlRepository.kt | 4 +- .../adcontrol/AdControlRepositoryImpl.kt | 10 +- .../adcontrol/AdControlRepositoryTest.kt | 391 +++++++++--------- .../client-repository-appconfig.gradle.kts | 1 - .../appconfig/AppConfigRepository.kt | 2 +- .../appconfig/AppConfigRepositoryImpl.kt | 4 +- .../appconfig/AppConfigRepositoryTest.kt | 78 ++-- .../storage/app/client-storage-app.gradle.kts | 1 - .../ccc/client/storage/app/AppStorage.kt | 12 +- .../ccc/client/storage/app/AppStorageImpl.kt | 35 +- .../ccc/client/storage/app/AppStorageTest.kt | 68 +-- .../client-storage-calculation.gradle.kts | 7 +- .../storage/calculation/CalculationStorage.kt | 13 +- .../calculation/CalculationStorageImpl.kt | 40 +- .../calculation/CalculationStorageTest.kt | 89 ++-- .../viewmodel/calculator/CalculatorSEED.kt | 3 +- .../calculator/CalculatorViewModel.kt | 161 ++++---- .../calculator/CalculatorViewModelTest.kt | 139 +++---- .../viewmodel/currencies/CurrenciesSEED.kt | 5 +- .../currencies/CurrenciesViewModel.kt | 26 +- .../currencies/CurrenciesViewModelTest.kt | 87 ++-- .../ccc/client/viewmodel/main/MainSEED.kt | 4 +- .../client/viewmodel/main/MainViewModel.kt | 54 ++- .../viewmodel/main/MainViewModelTest.kt | 150 ++++--- .../viewmodel/premium/PremiumViewModel.kt | 40 +- .../viewmodel/premium/PremiumViewModelTest.kt | 24 +- ...client-viewmodel-selectcurrency.gradle.kts | 4 - .../selectcurrency/SelectCurrencySEED.kt | 2 +- .../selectcurrency/SelectCurrencyViewModel.kt | 7 +- .../di/ClientViewModelSelectCurrencyModule.kt | 2 +- .../SelectCurrencyViewModelTest.kt | 13 +- .../client/viewmodel/settings/SettingsSEED.kt | 2 +- .../viewmodel/settings/SettingsViewModel.kt | 33 +- .../settings/SettingsViewModelTest.kt | 44 +- .../client/viewmodel/watchers/WatchersSEED.kt | 2 +- .../viewmodel/watchers/WatchersViewModel.kt | 28 +- .../watchers/WatchersViewModelTest.kt | 8 +- gradle/libs.versions.toml | 1 - .../UI/Calculator/CalculatorRootView.swift | 9 +- .../UI/Currencies/CurrenciesRootView.swift | 4 + ios/CCC/UI/Main/MainView.swift | 7 +- .../SelectCurrencyRootView.swift | 7 +- ios/CCC/UI/Settings/SettingsRootView.swift | 4 +- .../UI/Slides/BugReportSlideRootView.swift | 2 +- ios/CCC/UI/Watchers/WatcherItem.swift | 3 + ios/CCC/UI/Watchers/WatchersRootView.swift | 20 +- ios/CCC/UI/Watchers/WatchersView.swift | 2 + 68 files changed, 1017 insertions(+), 1220 deletions(-) delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt create mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt create mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt delete mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt create mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt delete mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt index d9fe1d548e..e1ed3b5424 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt @@ -19,6 +19,7 @@ import com.oztechan.ccc.android.ui.mobile.util.copyToClipBoard import com.oztechan.ccc.android.ui.mobile.util.dataState import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.getFromClipBoard +import com.oztechan.ccc.android.ui.mobile.util.getNavigationResult import com.oztechan.ccc.android.ui.mobile.util.setBackgroundByName import com.oztechan.ccc.android.ui.mobile.util.setBannerAd import com.oztechan.ccc.android.ui.mobile.util.showSnack @@ -56,6 +57,7 @@ class CalculatorFragment : BaseVBFragment() { binding.observeStates() binding.setListeners() observeEffects() + observeNavigationResults() } override fun onResume() { @@ -75,6 +77,14 @@ class CalculatorFragment : BaseVBFragment() { super.onDestroyView() } + private fun observeNavigationResults() = getNavigationResult( + CHANGE_BASE_EVENT, + R.id.calculatorFragment + )?.observe(viewLifecycleOwner) { + Logger.i { "CalculatorFragment observeNavigationResults $it" } + calculatorViewModel.event.onBaseChange(it) + } + private fun FragmentCalculatorBinding.initViews() = viewLifecycleOwner.lifecycleScope.launch { recyclerViewMain.adapter = calculatorAdapter } @@ -198,4 +208,8 @@ class CalculatorFragment : BaseVBFragment() { private fun Button.setKeyboardListener() = setOnClickListener { calculatorViewModel.event.onKeyPress(text.toString()) } + + companion object { + const val CHANGE_BASE_EVENT = "change_base" + } } diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt index 2d608ea0fa..040d7c868b 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt @@ -18,10 +18,12 @@ import com.github.submob.basemob.fragment.BaseVBFragment import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R +import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.FragmentCurrenciesBinding import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.hideKeyboard import com.oztechan.ccc.android.ui.mobile.util.setBannerAd +import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.showSnack import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager @@ -131,6 +133,12 @@ class CurrenciesFragment : BaseVBFragment() { findNavController().popBackStack() view?.hideKeyboard() } + + is CurrenciesEffect.ChangeBase -> setNavigationResult( + R.id.calculatorFragment, + viewEffect.newBase, + CHANGE_BASE_EVENT + ) } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt index 48726f8ccb..37acae6c32 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.activity.BaseActivity -import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R @@ -55,16 +54,12 @@ class MainActivity : BaseActivity() { .flowWithLifecycle(lifecycle) .onEach { with(it) { - shouldOnboardUser?.let { shouldOnboardUser -> - setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) - } + setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) // if dark mode is supported use theming according to user preference - it.appTheme - ?.whether { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q } - ?.let { appTheme -> - AppCompatDelegate.setDefaultNightMode(getThemeMode(appTheme)) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.setDefaultNightMode(getThemeMode(it.appTheme)) + } } }.launchIn(lifecycleScope) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt index 9adbe26e06..39876fc548 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt @@ -10,7 +10,9 @@ import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.bottomsheet.BaseVBBottomSheetDialogFragment import com.oztechan.ccc.android.ui.mobile.R +import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.BottomSheetSelectCurrencyBinding +import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager import com.oztechan.ccc.client.core.analytics.model.ScreenName @@ -83,7 +85,14 @@ class SelectCurrencyBottomSheet : .onEach { viewEffect -> Logger.i { "SelectCurrencyBottomSheet observeEffects ${viewEffect::class.simpleName}" } when (viewEffect) { - is SelectCurrencyEffect.DismissDialog -> dismissDialog() + is SelectCurrencyEffect.CurrencyChange -> { + setNavigationResult( + R.id.calculatorFragment, + viewEffect.newBase, + CHANGE_BASE_EVENT + ) + dismissDialog() + } SelectCurrencyEffect.OpenCurrencies -> navigate( R.id.selectCurrencyBottomSheet, diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt index 077759904a..f355573f7a 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt @@ -19,7 +19,10 @@ import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import com.github.submob.scopemob.castTo +import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.core.ad.BannerAdView import com.oztechan.ccc.android.ui.mobile.R @@ -70,6 +73,24 @@ fun View.animateHeight(startHeight: Int, endHeight: Int) { startAnimation(animation) } +fun Fragment.getNavigationResult( + key: String, + destinationId: Int +) = findNavController() + .currentBackStackEntry + ?.whether { it.destination.id == destinationId } + ?.savedStateHandle + ?.getLiveData(key) + +fun Fragment.setNavigationResult( + destinationId: Int, + result: T, + key: String +) = findNavController() + .previousBackStackEntry + ?.whether { it.destination.id == destinationId } + ?.savedStateHandle?.set(key, result) + fun View?.visibleIf(visible: Boolean, bringFront: Boolean = false) = this?.apply { if (visible) { isVisible = true diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt index 25561a7d14..37c6517ce9 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt @@ -10,8 +10,8 @@ import com.oztechan.ccc.common.core.model.Currency data class WidgetState( var currencyList: List = listOf(), var lastUpdate: String = "", - var currentBase: String = "", - var isPremium: Boolean = true + var currentBase: String, + var isPremium: Boolean ) : BaseState // Event diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt index f51ef727ea..b655b763ca 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt @@ -28,7 +28,12 @@ class WidgetViewModel( ) : BaseSEEDViewModel(), WidgetEvent { // region SEED - private val _state = MutableStateFlow(WidgetState()) + private val _state = MutableStateFlow( + WidgetState( + currentBase = calculationStorage.currentBase, + isPremium = appStorage.premiumEndDate.isNotPassed() + ) + ) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -39,24 +44,13 @@ class WidgetViewModel( override val data = WidgetData() // endregion - init { - viewModelScope.launchIgnored { - _state.update { - it.copy( - currentBase = calculationStorage.getBase(), - isPremium = appStorage.getPremiumEndDate().isNotPassed() - ) - } - } - } - - private suspend fun refreshWidgetData() { + private fun refreshWidgetData() { _state.update { it.copy( currencyList = listOf(), lastUpdate = "", - currentBase = calculationStorage.getBase(), - isPremium = appStorage.getPremiumEndDate().isNotPassed() + currentBase = calculationStorage.currentBase, + isPremium = appStorage.premiumEndDate.isNotPassed() ) } @@ -67,14 +61,14 @@ class WidgetViewModel( private fun getFreshWidgetData() = viewModelScope.launch { val conversion = backendApiService - .getConversion(calculationStorage.getBase()) + .getConversion(calculationStorage.currentBase) currencyDataSource.getActiveCurrencies() - .filterNot { it.code == calculationStorage.getBase() } + .filterNot { it.code == calculationStorage.currentBase } .onEach { - it.rate = conversion.getRateFromCode(it.code) - ?.getFormatted(calculationStorage.getPrecision()) - .orEmpty() + it.rate = + conversion.getRateFromCode(it.code)?.getFormatted(calculationStorage.precision) + .orEmpty() } .take(MAXIMUM_NUMBER_OF_CURRENCY) .let { currencyList -> @@ -92,7 +86,7 @@ class WidgetViewModel( val newBaseIndex = activeCurrencies .map { it.code } - .indexOf(calculationStorage.getBase()) + .indexOf(calculationStorage.currentBase) .let { if (isToNext) { it + 1 @@ -103,7 +97,7 @@ class WidgetViewModel( (it + activeCurrencies.size) % activeCurrencies.size // it handles index -1 and index size +1 } - calculationStorage.setBase(activeCurrencies[newBaseIndex].code) + calculationStorage.currentBase = activeCurrencies[newBaseIndex].code } // region Event diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index a4034663fb..4d47accedd 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -18,7 +18,9 @@ import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure +import io.mockative.every import io.mockative.mock +import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -80,16 +82,16 @@ internal class WidgetViewModelTest { val mockEndDate = Random.nextLong() - runTest { - coEvery { appStorage.getPremiumEndDate() } - .returns(mockEndDate) + every { appStorage.premiumEndDate } + .returns(mockEndDate) - coEvery { calculationStorage.getPrecision() } - .returns(3) + every { calculationStorage.currentBase } + .returns(base) - coEvery { calculationStorage.getBase() } - .returns(base) + every { calculationStorage.precision } + .returns(3) + runTest { coEvery { backendApiService.getConversion(base) } .returns(conversion) @@ -101,7 +103,7 @@ internal class WidgetViewModelTest { @Test fun `ArrayIndexOutOfBoundsException never thrown`() = runTest { // first currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(firstBase) coEvery { backendApiService.getConversion(firstBase) } @@ -118,7 +120,7 @@ internal class WidgetViewModelTest { } // middle currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(base) coEvery { backendApiService.getConversion(base) } @@ -135,7 +137,7 @@ internal class WidgetViewModelTest { } // last currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(lastBase) coEvery { backendApiService.getConversion(lastBase) } @@ -159,7 +161,7 @@ internal class WidgetViewModelTest { assertTrue { it.currencyList.isEmpty() } assertEquals("", it.lastUpdate) assertEquals(base, it.currentBase) - assertEquals(appStorage.getPremiumEndDate().isNotPassed(), it.isPremium) + assertEquals(appStorage.premiumEndDate.isNotPassed(), it.isPremium) } } @@ -170,7 +172,7 @@ internal class WidgetViewModelTest { @Test fun `if user is premium api call and db query are invoked`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -184,7 +186,7 @@ internal class WidgetViewModelTest { @Test fun `if user is not premium no api call and db query are not invoked`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -199,7 +201,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called all the conversion rates for currentBase is calculated`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.onSubscription { @@ -209,10 +211,7 @@ internal class WidgetViewModelTest { it.currencyList.forEach { currency -> conversion.getRateFromCode(currency.code).let { rate -> assertNotNull(rate) - assertEquals( - rate.getFormatted(calculationStorage.getPrecision()), - currency.rate - ) + assertEquals(rate.getFormatted(calculationStorage.precision), currency.rate) } } } @@ -221,7 +220,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called with null, base is not updated`() = runTest { // to not invoke getFreshWidgetData - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -229,7 +228,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasNotInvoked() - coVerify { calculationStorage.setBase(any()) } + verify { calculationStorage.currentBase = any() } .wasNotInvoked() } @@ -237,7 +236,7 @@ internal class WidgetViewModelTest { @Test fun onNextClick() = runTest { // when onNextClick, base is updated next or the first active currency - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onNextClick() @@ -245,10 +244,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(lastBase) } + verify { calculationStorage.currentBase = lastBase } .wasInvoked() - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(lastBase) viewModel.event.onNextClick() @@ -256,14 +255,14 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(firstBase) } + verify { calculationStorage.currentBase = firstBase } .wasInvoked() } @Test fun onPreviousClick() = runTest { // when onRefreshClick, base is updated previous or the last active currency - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onPreviousClick() @@ -271,10 +270,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(firstBase) } + verify { calculationStorage.currentBase = firstBase } .wasInvoked() - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(firstBase) viewModel.event.onPreviousClick() @@ -282,13 +281,13 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(lastBase) } + verify { calculationStorage.currentBase = lastBase } .wasInvoked() } @Test fun onRefreshClick() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -299,7 +298,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.getBase() } + verify { calculationStorage.currentBase } .wasInvoked() } diff --git a/client/core/persistence/client-core-persistence.gradle.kts b/client/core/persistence/client-core-persistence.gradle.kts index 2aaadca6ba..ada4a720d1 100644 --- a/client/core/persistence/client-core-persistence.gradle.kts +++ b/client/core/persistence/client-core-persistence.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { libs.plugins.apply { alias(kotlinMultiplatform) @@ -16,29 +14,19 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(Modules.Common.Core.infrastructure)) libs.common.apply { implementation(koinCore) - implementation(coroutines) implementation(multiplatformSettings) - implementation(multiplatformSettingsCoroutines) } } commonTest.dependencies { libs.common.apply { implementation(test) implementation(mockative) - implementation(coroutinesTest) } } } } -// todo remove after https://github.com/russhwolf/multiplatform-settings/issues/119 -tasks.withType { - kotlinOptions { - freeCompilerArgs += "-opt-in=com.russhwolf.settings.ExperimentalSettingsApi" - } -} dependencies { configurations diff --git a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt index 2ef23b2967..784241f725 100644 --- a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt +++ b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt @@ -2,32 +2,20 @@ package com.oztechan.ccc.client.core.persistence.di import android.content.Context import android.content.SharedPreferences -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl -import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO -import com.russhwolf.settings.ObservableSettings +import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.PersistenceImpl +import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings -import com.russhwolf.settings.coroutines.toFlowSettings -import com.russhwolf.settings.coroutines.toSuspendSettings +import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named import org.koin.dsl.module private const val KEY_APPLICATION_PREFERENCES = "application_preferences" actual val clientCorePersistenceModule = module { singleOf(::provideSharedPreferences) - - single { SharedPreferencesSettings(get()) } - - single { - FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) - } - single { - SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) - } + single { SharedPreferencesSettings(get()) } + singleOf(::PersistenceImpl) { bind() } } private fun provideSharedPreferences( diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt deleted file mode 100644 index 78830b67b1..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import kotlinx.coroutines.flow.Flow - -interface FlowPersistence { - fun getFlow(key: String, defaultValue: T): Flow -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt deleted file mode 100644 index e99fbe013b..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.russhwolf.settings.coroutines.FlowSettings -import kotlinx.coroutines.flow.Flow - -@Suppress("OPT_IN_USAGE") -internal class FlowPersistenceImpl(private val flowSettings: FlowSettings) : FlowPersistence { - @Suppress("UNCHECKED_CAST") - override fun getFlow(key: String, defaultValue: T): Flow = - when (defaultValue) { - is Long -> flowSettings.getLongFlow(key, defaultValue) as Flow - is String -> flowSettings.getStringFlow(key, defaultValue) as Flow - is Int -> flowSettings.getIntFlow(key, defaultValue) as Flow - is Boolean -> flowSettings.getBooleanFlow(key, defaultValue) as Flow - is Float -> flowSettings.getFloatFlow(key, defaultValue) as Flow - else -> throw UnsupportedPersistenceException() - } -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt new file mode 100644 index 0000000000..77002313a1 --- /dev/null +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt @@ -0,0 +1,6 @@ +package com.oztechan.ccc.client.core.persistence + +interface Persistence { + fun getValue(key: String, defaultValue: T): T + fun setValue(key: String, value: T) +} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt new file mode 100644 index 0000000000..a2747caf61 --- /dev/null +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt @@ -0,0 +1,25 @@ +package com.oztechan.ccc.client.core.persistence + +import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException +import com.russhwolf.settings.Settings + +internal class PersistenceImpl(private val settings: Settings) : Persistence { + @Suppress("UNCHECKED_CAST") + override fun getValue(key: String, defaultValue: T): T = when (defaultValue) { + is Long -> settings.getLong(key, defaultValue) as T + is String -> settings.getString(key, defaultValue) as T + is Int -> settings.getInt(key, defaultValue) as T + is Boolean -> settings.getBoolean(key, defaultValue) as T + is Float -> settings.getFloat(key, defaultValue) as T + else -> throw UnsupportedPersistenceException() + } + + override fun setValue(key: String, value: T) = when (value) { + is Long -> settings.putLong(key, value) + is String -> settings.putString(key, value) + is Int -> settings.putInt(key, value) + is Boolean -> settings.putBoolean(key, value) + is Float -> settings.putFloat(key, value) + else -> throw UnsupportedPersistenceException() + } +} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt deleted file mode 100644 index bb4e617c04..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -interface SuspendPersistence { - suspend fun getSuspend(key: String, defaultValue: T): T - suspend fun setSuspend(key: String, value: T) -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt deleted file mode 100644 index 9c905f492e..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.russhwolf.settings.coroutines.SuspendSettings - -@Suppress("OPT_IN_USAGE") -internal class SuspendPersistenceImpl(private val suspendSettings: SuspendSettings) : - SuspendPersistence { - - @Suppress("UNCHECKED_CAST") - override suspend fun getSuspend(key: String, defaultValue: T): T = - when (defaultValue) { - is Long -> suspendSettings.getLong(key, defaultValue) as T - is String -> suspendSettings.getString(key, defaultValue) as T - is Int -> suspendSettings.getInt(key, defaultValue) as T - is Boolean -> suspendSettings.getBoolean(key, defaultValue) as T - is Float -> suspendSettings.getFloat(key, defaultValue) as T - else -> throw UnsupportedPersistenceException() - } - - override suspend fun setSuspend(key: String, value: T) = when (value) { - is Long -> suspendSettings.putLong(key, value) - is String -> suspendSettings.putString(key, value) - is Int -> suspendSettings.putInt(key, value) - is Boolean -> suspendSettings.putBoolean(key, value) - is Float -> suspendSettings.putFloat(key, value) - else -> throw UnsupportedPersistenceException() - } -} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt deleted file mode 100644 index 48df14a797..0000000000 --- a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString -import com.russhwolf.settings.coroutines.FlowSettings -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.configure -import io.mockative.every -import io.mockative.mock -import io.mockative.verify -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@Suppress("OPT_IN_USAGE") -internal class FlowPersistenceTest { - private val flowPersistence: FlowPersistence by lazy { - FlowPersistenceImpl(flowSettings) - } - - @Mock - private val flowSettings = configure(mock(classOf())) { - stubsUnitByDefault = true - } - - @Test - fun `getFlow returns the same type`() = runTest { - every { flowSettings.getFloatFlow(KEY, mockFloat) } - .returns(flowOf(mockFloat)) - every { flowSettings.getBooleanFlow(KEY, mockBoolean) } - .returns(flowOf(mockBoolean)) - every { flowSettings.getIntFlow(KEY, mockInt) } - .returns(flowOf(mockInt)) - every { flowSettings.getStringFlow(KEY, mockString) } - .returns(flowOf(mockString)) - every { flowSettings.getLongFlow(KEY, mockLong) } - .returns(flowOf(mockLong)) - - assertEquals(mockFloat, flowPersistence.getFlow(KEY, mockFloat).firstOrNull()) - assertEquals(mockBoolean, flowPersistence.getFlow(KEY, mockBoolean).firstOrNull()) - assertEquals(mockInt, flowPersistence.getFlow(KEY, mockInt).firstOrNull()) - assertEquals(mockString, flowPersistence.getFlow(KEY, mockString).firstOrNull()) - assertEquals(mockLong, flowPersistence.getFlow(KEY, mockLong).firstOrNull()) - - verify { flowSettings.getFloatFlow(KEY, mockFloat) } - .wasInvoked() - verify { flowSettings.getBooleanFlow(KEY, mockBoolean) } - .wasInvoked() - verify { flowSettings.getIntFlow(KEY, mockInt) } - .wasInvoked() - verify { flowSettings.getStringFlow(KEY, mockString) } - .wasInvoked() - verify { flowSettings.getLongFlow(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `UnsupportedPersistenceException throw when unsupported type tried to read`() = - runTest { - val mockObject = object {} - - assertFailsWith(UnsupportedPersistenceException::class) { - flowPersistence.getFlow(KEY, mockObject) - } - } -} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt new file mode 100644 index 0000000000..fbbccc21bc --- /dev/null +++ b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt @@ -0,0 +1,91 @@ +package com.oztechan.ccc.client.core.persistence + +import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString +import com.russhwolf.settings.Settings +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.configure +import io.mockative.every +import io.mockative.mock +import io.mockative.verify +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +internal class PersistenceTest { + private val persistence: Persistence by lazy { + PersistenceImpl(settings) + } + + @Mock + private val settings = configure(mock(classOf())) { stubsUnitByDefault = true } + + @Test + fun `getValue returns the same type`() { + every { settings.getFloat(KEY, mockFloat) } + .returns(mockFloat) + every { settings.getBoolean(KEY, mockBoolean) } + .returns(mockBoolean) + every { settings.getInt(KEY, mockInt) } + .returns(mockInt) + every { settings.getString(KEY, mockString) } + .returns(mockString) + every { settings.getLong(KEY, mockLong) } + .returns(mockLong) + + assertEquals(mockFloat, persistence.getValue(KEY, mockFloat)) + assertEquals(mockBoolean, persistence.getValue(KEY, mockBoolean)) + assertEquals(mockInt, persistence.getValue(KEY, mockInt)) + assertEquals(mockString, persistence.getValue(KEY, mockString)) + assertEquals(mockLong, persistence.getValue(KEY, mockLong)) + + verify { settings.getFloat(KEY, mockFloat) } + .wasInvoked() + verify { settings.getBoolean(KEY, mockBoolean) } + .wasInvoked() + verify { settings.getInt(KEY, mockInt) } + .wasInvoked() + verify { settings.getString(KEY, mockString) } + .wasInvoked() + verify { settings.getLong(KEY, mockLong) } + .wasInvoked() + } + + @Test + fun `setValue sets the same type`() { + persistence.setValue(KEY, mockFloat) + persistence.setValue(KEY, mockBoolean) + persistence.setValue(KEY, mockInt) + persistence.setValue(KEY, mockString) + persistence.setValue(KEY, mockLong) + + verify { settings.putFloat(KEY, mockFloat) } + .wasInvoked() + verify { settings.putBoolean(KEY, mockBoolean) } + .wasInvoked() + verify { settings.putInt(KEY, mockInt) } + .wasInvoked() + verify { settings.putString(KEY, mockString) } + .wasInvoked() + verify { settings.putLong(KEY, mockLong) } + .wasInvoked() + } + + @Test + fun `setValue throw UnsupportedPersistenceException when unsupported type tried to saved or read`() { + val mockObject = object {} + + assertFailsWith(UnsupportedPersistenceException::class) { + persistence.setValue(KEY, mockObject) + } + assertFailsWith(UnsupportedPersistenceException::class) { + persistence.getValue(KEY, mockObject) + } + } +} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt deleted file mode 100644 index 6cc32d4b29..0000000000 --- a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString -import com.russhwolf.settings.coroutines.SuspendSettings -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify -import io.mockative.configure -import io.mockative.mock -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@Suppress("OPT_IN_USAGE") -internal class SuspendPersistenceTest { - private val suspendPersistence: SuspendPersistence by lazy { - SuspendPersistenceImpl(suspendSettings) - } - - @Mock - private val suspendSettings = configure(mock(classOf())) { - stubsUnitByDefault = true - } - - @Test - fun `getSuspend returns the same type`() = runTest { - coEvery { suspendSettings.getFloat(KEY, mockFloat) } - .returns(mockFloat) - coEvery { suspendSettings.getBoolean(KEY, mockBoolean) } - .returns(mockBoolean) - coEvery { suspendSettings.getInt(KEY, mockInt) } - .returns(mockInt) - coEvery { suspendSettings.getString(KEY, mockString) } - .returns(mockString) - coEvery { suspendSettings.getLong(KEY, mockLong) } - .returns(mockLong) - - assertEquals(mockFloat, suspendPersistence.getSuspend(KEY, mockFloat)) - assertEquals(mockBoolean, suspendPersistence.getSuspend(KEY, mockBoolean)) - assertEquals(mockInt, suspendPersistence.getSuspend(KEY, mockInt)) - assertEquals(mockString, suspendPersistence.getSuspend(KEY, mockString)) - assertEquals(mockLong, suspendPersistence.getSuspend(KEY, mockLong)) - - coVerify { suspendSettings.getFloat(KEY, mockFloat) } - .wasInvoked() - coVerify { suspendSettings.getBoolean(KEY, mockBoolean) } - .wasInvoked() - coVerify { suspendSettings.getInt(KEY, mockInt) } - .wasInvoked() - coVerify { suspendSettings.getString(KEY, mockString) } - .wasInvoked() - coVerify { suspendSettings.getLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `setSuspend sets the same type`() = runTest { - suspendPersistence.setSuspend(KEY, mockFloat) - suspendPersistence.setSuspend(KEY, mockBoolean) - suspendPersistence.setSuspend(KEY, mockInt) - suspendPersistence.setSuspend(KEY, mockString) - suspendPersistence.setSuspend(KEY, mockLong) - - coVerify { suspendSettings.putFloat(KEY, mockFloat) } - .wasInvoked() - coVerify { suspendSettings.putBoolean(KEY, mockBoolean) } - .wasInvoked() - coVerify { suspendSettings.putInt(KEY, mockInt) } - .wasInvoked() - coVerify { suspendSettings.putString(KEY, mockString) } - .wasInvoked() - coVerify { suspendSettings.putLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `UnsupportedPersistenceException throw when unsupported type tried to saved or read`() = - runTest { - val mockObject = object {} - - assertFailsWith(UnsupportedPersistenceException::class) { - suspendPersistence.setSuspend(KEY, mockObject) - } - assertFailsWith(UnsupportedPersistenceException::class) { - suspendPersistence.getSuspend(KEY, mockObject) - } - } -} diff --git a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt index d5e7ce4790..1e01c68c4a 100644 --- a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt +++ b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt @@ -1,28 +1,16 @@ package com.oztechan.ccc.client.core.persistence.di -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl -import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO +import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.PersistenceImpl import com.russhwolf.settings.NSUserDefaultsSettings -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.toFlowSettings -import com.russhwolf.settings.coroutines.toSuspendSettings -import org.koin.core.qualifier.named +import com.russhwolf.settings.Settings +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module actual val clientCorePersistenceModule = module { - single { + single { NSUserDefaultsSettings(get().userDefaults) } - - @Suppress("OPT_IN_USAGE") - single { - FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) - } - @Suppress("OPT_IN_USAGE") - single { - SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) - } + singleOf(::PersistenceImpl) { bind() } } diff --git a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts index 3f265ab01d..3cddcc191c 100644 --- a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts +++ b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts @@ -23,7 +23,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt index 4aed693e02..e4957fd268 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt @@ -1,7 +1,7 @@ package com.oztechan.ccc.client.repository.adcontrol interface AdControlRepository { - suspend fun shouldShowBannerAd(): Boolean + fun shouldShowBannerAd(): Boolean - suspend fun shouldShowInterstitialAd(): Boolean + fun shouldShowInterstitialAd(): Boolean } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt index e102aa1365..cf4fc5c4dc 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt @@ -8,10 +8,10 @@ internal class AdControlRepositoryImpl( private val appStorage: AppStorage, private val adConfigService: AdConfigService ) : AdControlRepository { - override suspend fun shouldShowBannerAd() = !appStorage.isFirstRun() && - appStorage.getPremiumEndDate().isPassed() && - appStorage.getSessionCount() > adConfigService.config.bannerAdSessionCount + override fun shouldShowBannerAd() = !appStorage.firstRun && + appStorage.premiumEndDate.isPassed() && + appStorage.sessionCount > adConfigService.config.bannerAdSessionCount - override suspend fun shouldShowInterstitialAd() = appStorage.getPremiumEndDate().isPassed() && - appStorage.getSessionCount() > adConfigService.config.interstitialAdSessionCount + override fun shouldShowInterstitialAd() = appStorage.premiumEndDate.isPassed() && + appStorage.sessionCount > adConfigService.config.interstitialAdSessionCount } diff --git a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt index 512740a6b8..ed906268aa 100644 --- a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt +++ b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt @@ -6,12 +6,9 @@ import com.oztechan.ccc.client.core.shared.util.nowAsLong import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify -import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.BeforeTest import kotlin.test.Test @@ -40,302 +37,290 @@ internal class AdControlRepositoryTest { } @Test - fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertTrue { subject.shouldShowBannerAd() } + assertTrue { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config.interstitialAdSessionCount } - .wasNotInvoked() + verify { adConfigService.config.interstitialAdSessionCount } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertTrue { subject.shouldShowInterstitialAd() } + assertTrue { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasNotInvoked() + verify { adConfigService.config } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } } diff --git a/client/repository/appconfig/client-repository-appconfig.gradle.kts b/client/repository/appconfig/client-repository-appconfig.gradle.kts index 46eedb0a02..b4f49c5b0a 100644 --- a/client/repository/appconfig/client-repository-appconfig.gradle.kts +++ b/client/repository/appconfig/client-repository-appconfig.gradle.kts @@ -33,7 +33,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt index e05c5ee075..21bd936fe5 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt @@ -10,7 +10,7 @@ interface AppConfigRepository { fun checkAppUpdate(isAppUpdateShown: Boolean): Boolean? - suspend fun shouldShowAppReview(): Boolean + fun shouldShowAppReview(): Boolean fun getVersion(): String } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt index f018586c0b..f71b37eea8 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt @@ -27,8 +27,8 @@ internal class AppConfigRepositoryImpl( it.updateForceVersion > BuildKonfig.versionCode } ?: run { null } // do not show - override suspend fun shouldShowAppReview(): Boolean = reviewConfigService.config - .whether { appStorage.getSessionCount() > it.appReviewSessionCount } + override fun shouldShowAppReview(): Boolean = reviewConfigService.config + .whether { appStorage.sessionCount > it.appReviewSessionCount } ?.mapTo { true } ?: false diff --git a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt index a050858b72..b3b39abc03 100644 --- a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt +++ b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt @@ -8,12 +8,9 @@ import com.oztechan.ccc.client.core.shared.Device import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify -import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -111,64 +108,61 @@ internal class AppConfigRepositoryTest { } @Test - fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong() + 1) + every { appStorage.sessionCount } + .returns(mockInteger.toLong() + 1) - assertTrue { subject.shouldShowAppReview() } + assertTrue { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong() - 1) + every { appStorage.sessionCount } + .returns(mockInteger.toLong() - 1) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong()) + every { appStorage.sessionCount } + .returns(mockInteger.toLong()) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test fun getVersion() { diff --git a/client/storage/app/client-storage-app.gradle.kts b/client/storage/app/client-storage-app.gradle.kts index 3d1aab333e..5b4d634d95 100644 --- a/client/storage/app/client-storage-app.gradle.kts +++ b/client/storage/app/client-storage-app.gradle.kts @@ -20,7 +20,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index 34f4e1d378..cdb333376c 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -1,15 +1,11 @@ package com.oztechan.ccc.client.storage.app interface AppStorage { - suspend fun isFirstRun(): Boolean - suspend fun setFirstRun(value: Boolean) + var firstRun: Boolean - suspend fun getAppTheme(): Int - suspend fun setAppTheme(value: Int) + var appTheme: Int - suspend fun getPremiumEndDate(): Long - suspend fun setPremiumEndDate(value: Long) + var premiumEndDate: Long - suspend fun getSessionCount(): Long - suspend fun setSessionCount(value: Long) + var sessionCount: Long } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index df3a65c687..84d334c55e 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -1,33 +1,26 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence internal class AppStorageImpl( - private val suspendPersistence: SuspendPersistence + private val persistence: Persistence ) : AppStorage { - override suspend fun isFirstRun(): Boolean = - suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) - override suspend fun setFirstRun(value: Boolean) = - suspendPersistence.setSuspend(KEY_FIRST_RUN, value) + override var firstRun + get() = persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) + set(value) = persistence.setValue(KEY_FIRST_RUN, value) - override suspend fun getAppTheme(): Int = - suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) + override var appTheme + get() = persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) + set(value) = persistence.setValue(KEY_APP_THEME, value) - override suspend fun setAppTheme(value: Int) = - suspendPersistence.setSuspend(KEY_APP_THEME, value) + override var premiumEndDate + get() = persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) + set(value) = persistence.setValue(KEY_PREMIUM_END_DATE, value) - override suspend fun getPremiumEndDate(): Long = - suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) - - override suspend fun setPremiumEndDate(value: Long) = - suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, value) - - override suspend fun getSessionCount(): Long = - suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) - - override suspend fun setSessionCount(value: Long) = - suspendPersistence.setSuspend(KEY_SESSION_COUNT, value) + override var sessionCount: Long + get() = persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) + set(value) = persistence.setValue(KEY_SESSION_COUNT, value) companion object { internal const val KEY_FIRST_RUN = "firs_run" diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index 76ddf4c883..724b462ba0 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -1,6 +1,6 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_APP_THEME import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_FIRST_RUN import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_PREMIUM_END_DATE @@ -11,10 +11,10 @@ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_PREMIUM_ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_SESSION_COUNT import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify +import io.mockative.configure +import io.mockative.every import io.mockative.mock -import kotlinx.coroutines.test.runTest +import io.mockative.verify import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -22,91 +22,91 @@ import kotlin.test.assertEquals internal class AppStorageTest { private val subject: AppStorage by lazy { - AppStorageImpl(suspendPersistence) + AppStorageImpl(persistence) } @Mock - private val suspendPersistence = mock(classOf()) + private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } // defaults @Test - fun `get default firstRun`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + fun `default firstRun`() { + every { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .returns(DEFAULT_FIRST_RUN) - assertEquals(DEFAULT_FIRST_RUN, subject.isFirstRun()) + assertEquals(DEFAULT_FIRST_RUN, subject.firstRun) - coVerify { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + verify { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .wasInvoked() } @Test - fun `get default appTheme`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } + fun `default appTheme`() { + every { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } .returns(DEFAULT_APP_THEME) - assertEquals(DEFAULT_APP_THEME, subject.getAppTheme()) + assertEquals(DEFAULT_APP_THEME, subject.appTheme) - coVerify { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } + verify { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } .wasInvoked() } @Test - fun `get default premiumEndDate`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + fun `default premiumEndDate`() { + every { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .returns(DEFAULT_PREMIUM_END_DATE) - assertEquals(DEFAULT_PREMIUM_END_DATE, subject.getPremiumEndDate()) + assertEquals(DEFAULT_PREMIUM_END_DATE, subject.premiumEndDate) - coVerify { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + verify { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .wasInvoked() } @Test - fun `get default sessionCount`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + fun `default sessionCount`() { + every { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .returns(DEFAULT_SESSION_COUNT) - assertEquals(DEFAULT_SESSION_COUNT, subject.getSessionCount()) + assertEquals(DEFAULT_SESSION_COUNT, subject.sessionCount) - coVerify { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + verify { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .wasInvoked() } // setters @Test - fun `set firstRun`() = runTest { + fun `set firstRun`() { val mockedValue = Random.nextBoolean() - subject.setFirstRun(mockedValue) + subject.firstRun = mockedValue - coVerify { suspendPersistence.setSuspend(KEY_FIRST_RUN, mockedValue) } + verify { persistence.setValue(KEY_FIRST_RUN, mockedValue) } .wasInvoked() } @Test - fun `set appTheme`() = runTest { + fun `set appTheme`() { val mockValue = Random.nextInt() - subject.setAppTheme(mockValue) + subject.appTheme = mockValue - coVerify { suspendPersistence.setSuspend(KEY_APP_THEME, mockValue) } + verify { persistence.setValue(KEY_APP_THEME, mockValue) } .wasInvoked() } @Test - fun `set premiumEndDate`() = runTest { + fun `set premiumEndDate`() { val mockValue = Random.nextLong() - subject.setPremiumEndDate(mockValue) + subject.premiumEndDate = mockValue - coVerify { suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, mockValue) } + verify { persistence.setValue(KEY_PREMIUM_END_DATE, mockValue) } .wasInvoked() } @Test - fun `set sessionCount`() = runTest { + fun `set sessionCount`() { val mockValue = Random.nextLong() - subject.setSessionCount(mockValue) + subject.sessionCount = mockValue - coVerify { suspendPersistence.setSuspend(KEY_SESSION_COUNT, mockValue) } + verify { persistence.setValue(KEY_SESSION_COUNT, mockValue) } .wasInvoked() } } diff --git a/client/storage/calculation/client-storage-calculation.gradle.kts b/client/storage/calculation/client-storage-calculation.gradle.kts index 2ccc3b6831..024143a5fb 100644 --- a/client/storage/calculation/client-storage-calculation.gradle.kts +++ b/client/storage/calculation/client-storage-calculation.gradle.kts @@ -14,17 +14,12 @@ kotlin { sourceSets { commonMain.dependencies { + implementation(libs.common.koinCore) implementation(project(Modules.Client.Core.persistence)) - - libs.common.apply { - implementation(koinCore) - implementation(coroutines) - } } commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index a4f7c1b7c6..6156eb61e8 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -1,16 +1,9 @@ package com.oztechan.ccc.client.storage.calculation -import kotlinx.coroutines.flow.Flow - interface CalculationStorage { - fun getBaseFlow(): Flow - suspend fun getBase(): String - suspend fun setBase(value: String) + var currentBase: String - fun getLastInputFlow(): Flow - suspend fun getLastInput(): String - suspend fun setLastInput(value: String) + var precision: Int - suspend fun getPrecision(): Int - suspend fun setPrecision(value: Int) + var lastInput: String } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index 4ab52552b4..03a0a59a85 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -1,44 +1,30 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import kotlinx.coroutines.flow.Flow +import com.oztechan.ccc.client.core.persistence.Persistence internal class CalculationStorageImpl( - private val suspendPersistence: SuspendPersistence, - private val flowPersistence: FlowPersistence + private val persistence: Persistence ) : CalculationStorage { - override fun getBaseFlow(): Flow = - flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) - override suspend fun getBase(): String = - suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) + override var currentBase + get() = persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) + set(value) = persistence.setValue(KEY_CURRENT_BASE, value) - override suspend fun setBase(value: String) = - suspendPersistence.setSuspend(KEY_CURRENT_BASE, value) + override var precision: Int + get() = persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) + set(value) = persistence.setValue(KEY_PRECISION, value) - override fun getLastInputFlow(): Flow = - flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) - - override suspend fun getLastInput(): String = - suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) - - override suspend fun setLastInput(value: String) = - suspendPersistence.setSuspend(KEY_LAST_INPUT, value) - - override suspend fun getPrecision(): Int = - suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) - - override suspend fun setPrecision(value: Int) = - suspendPersistence.setSuspend(KEY_PRECISION, value) + override var lastInput: String + get() = persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) + set(value) = persistence.setValue(KEY_LAST_INPUT, value) companion object { internal const val DEFAULT_CURRENT_BASE = "" - internal const val DEFAULT_LAST_INPUT = "" internal const val DEFAULT_PRECISION = 3 + internal const val DEFAULT_LAST_INPUT = "" internal const val KEY_CURRENT_BASE = "current_base" - internal const val KEY_LAST_INPUT = "last_input" internal const val KEY_PRECISION = "precision" + internal const val KEY_LAST_INPUT = "last_input" } } diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index 801e7ef5ae..d37a563dde 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -1,7 +1,6 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_CURRENT_BASE import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_LAST_INPUT import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_PRECISION @@ -10,109 +9,81 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Compan import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.KEY_PRECISION import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify +import io.mockative.configure import io.mockative.every import io.mockative.mock -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest +import io.mockative.verify import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals internal class CalculationStorageTest { private val subject: CalculationStorage by lazy { - CalculationStorageImpl(suspendPersistence, flowPersistence) + CalculationStorageImpl(persistence) } @Mock - private val suspendPersistence = mock(classOf()) - - @Mock - private val flowPersistence = mock(classOf()) - - @Test - fun getBaseFlow() = runTest { - every { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } - .returns(flowOf(DEFAULT_CURRENT_BASE)) - - assertEquals(DEFAULT_CURRENT_BASE, subject.getBaseFlow().first()) - - coVerify { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } - .wasInvoked() - } - - @Test - fun getLastInputFlow() = runTest { - every { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .returns(flowOf(DEFAULT_LAST_INPUT)) - - assertEquals(DEFAULT_LAST_INPUT, subject.getLastInputFlow().first()) - - coVerify { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .wasInvoked() - } + private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } // defaults @Test - fun `get default base`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + fun `default currentBase`() { + every { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .returns(DEFAULT_CURRENT_BASE) - assertEquals(DEFAULT_CURRENT_BASE, subject.getBase()) + assertEquals(DEFAULT_CURRENT_BASE, subject.currentBase) - coVerify { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + verify { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .wasInvoked() } @Test - fun `get default lastInput`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .returns(DEFAULT_LAST_INPUT) + fun `default precision`() { + every { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } + .returns(DEFAULT_PRECISION) - assertEquals(DEFAULT_LAST_INPUT, subject.getLastInput()) + assertEquals(DEFAULT_PRECISION, subject.precision) - coVerify { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + verify { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } .wasInvoked() } @Test - fun `get default precision`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } - .returns(DEFAULT_PRECISION) + fun `default lastInput`() { + every { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + .returns(DEFAULT_LAST_INPUT) - assertEquals(DEFAULT_PRECISION, subject.getPrecision()) + assertEquals(DEFAULT_LAST_INPUT, subject.lastInput) - coVerify { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } + verify { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } .wasInvoked() } // setters @Test - fun `set base`() = runTest { + fun `set currentBase`() { val mockValue = "mock" - subject.setBase(mockValue) + subject.currentBase = mockValue - coVerify { suspendPersistence.setSuspend(KEY_CURRENT_BASE, mockValue) } + verify { persistence.setValue(KEY_CURRENT_BASE, mockValue) } .wasInvoked() } @Test - fun `set lastInput`() = runTest { - val mockValue = "mock" - subject.setLastInput(mockValue) + fun `set precision`() { + val mockValue = Random.nextInt() + subject.precision = mockValue - coVerify { suspendPersistence.setSuspend(KEY_LAST_INPUT, mockValue) } + verify { persistence.setValue(KEY_PRECISION, mockValue) } .wasInvoked() } @Test - fun `set precision`() = runTest { - val mockValue = Random.nextInt() - subject.setPrecision(mockValue) + fun `set lastInput`() { + val mockValue = "mock" + subject.lastInput = mockValue - coVerify { suspendPersistence.setSuspend(KEY_PRECISION, mockValue) } + verify { persistence.setValue(KEY_LAST_INPUT, mockValue) } .wasInvoked() } } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt index 5cb8c1bb21..172bb59b64 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt @@ -11,7 +11,7 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CalculatorState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val input: String = "", val base: String = "", val currencyList: List = listOf(), @@ -32,6 +32,7 @@ interface CalculatorEvent : BaseEvent { fun onPasteToInput(text: String) fun onBarClick() fun onSettingsClicked() + fun onBaseChange(base: String) } // Effect diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 758c08bac8..cbc7bc9d8c 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -34,15 +34,15 @@ import com.oztechan.ccc.client.viewmodel.calculator.util.getConversionStringFrom import com.oztechan.ccc.common.core.model.Conversion import com.oztechan.ccc.common.core.model.Currency import com.oztechan.ccc.common.datasource.conversion.ConversionDataSource -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -56,7 +56,8 @@ class CalculatorViewModel( ) : BaseSEEDViewModel(), CalculatorEvent { // region SEED - private val _state = MutableStateFlow(CalculatorState()) + private val _state = + MutableStateFlow(CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -68,51 +69,53 @@ class CalculatorViewModel( // endregion init { - combine( - currencyDataSource.getActiveCurrenciesFlow(), - calculationStorage.getBaseFlow(), - state.map { it.input }.distinctUntilChanged(), - ) { activeCurrencies, base, input -> - Logger.d { - "CalculatorViewModel combined: " + - "base: $base, input: $input, activeCurrencies: ${activeCurrencies.joinToString(",") { it.code }}" + currencyDataSource.getActiveCurrenciesFlow() + .onStart { + _state.update { + copy( + currencyList = currencyDataSource.getActiveCurrencies(), + base = calculationStorage.currentBase, + input = calculationStorage.lastInput, + loading = true + ) + } + updateConversion() + observeBase() + observeInput() } + .onEach { + Logger.d { "CalculatorViewModel currencyList changed: ${it.joinToString(",")}" } + _state.update { copy(currencyList = it) } - _state.update { copy(currencyList = activeCurrencies) } - - analyticsManager.setUserProperty( - UserProperty.CurrencyCount(activeCurrencies.count().toString()) - ) - - if (state.value.base != base) { - currentBaseChanged(base, input) + analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) } + .launchIn(viewModelScope) + } - calculateOutput(input) - calculationStorage.setLastInput(input) - }.launchIn(viewModelScope) - - viewModelScope.launch { - _state.update { - copy( - currencyList = currencyDataSource.getActiveCurrencies(), - base = calculationStorage.getBase(), - input = calculationStorage.getLastInput(), - symbol = currencyDataSource.getCurrencyByCode(calculationStorage.getBase())?.symbol.orEmpty(), - loading = true, - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } + private fun observeBase() = state.map { it.base } + .distinctUntilChanged() + .onEach { + Logger.d { "CalculatorViewModel observeBase $it" } + currentBaseChanged(it, true) } - } + .launchIn(viewModelScope) + + private fun observeInput() = state.map { it.input } + .distinctUntilChanged() + .onEach { + Logger.d { "CalculatorViewModel observeInput $it" } + calculationStorage.lastInput = it + calculateOutput(it) + } + .launchIn(viewModelScope) - private suspend fun updateConversion() { + private fun updateConversion() { _state.update { copy(loading = true) } data.conversion?.let { calculateConversions(it, ConversionState.Cached(it.date)) } ?: viewModelScope.launch { - runCatching { backendApiService.getConversion(calculationStorage.getBase()) } + runCatching { backendApiService.getConversion(calculationStorage.currentBase) } .onFailure(::updateConversionFailed) .onSuccess(::updateConversionSuccess) } @@ -122,8 +125,9 @@ class CalculatorViewModel( conversion.copy(date = nowAsDateString()) .let { data.conversion = it + calculateConversions(it, ConversionState.Online(it.date)) + viewModelScope.launch { - calculateConversions(it, ConversionState.Online(it.date)) conversionDataSource.insertConversion(it) } } @@ -131,7 +135,7 @@ class CalculatorViewModel( private fun updateConversionFailed(t: Throwable) = viewModelScope.launchIgnored { Logger.w(t) { "CalculatorViewModel updateConversionFailed" } conversionDataSource.getConversionByBase( - calculationStorage.getBase() + calculationStorage.currentBase )?.let { calculateConversions(it, ConversionState.Offline(it.date)) } ?: run { @@ -143,25 +147,23 @@ class CalculatorViewModel( } } - private suspend fun calculateConversions( - conversion: Conversion?, - conversionState: ConversionState - ) = _state.update { - copy( - currencyList = _state.value.currencyList.onEach { - it.rate = conversion.calculateRate(it.code, _state.value.output) - .getFormatted(calculationStorage.getPrecision()) - .toStandardDigits() - }, - conversionState = conversionState, - loading = false - ) - } + private fun calculateConversions(conversion: Conversion?, conversionState: ConversionState) = + _state.update { + copy( + currencyList = _state.value.currencyList.onEach { + it.rate = conversion.calculateRate(it.code, _state.value.output) + .getFormatted(calculationStorage.precision) + .toStandardDigits() + }, + conversionState = conversionState, + loading = false + ) + } private fun calculateOutput(input: String) = viewModelScope.launch { val output = data.parser .calculate(input.toSupportedCharacters(), MAXIMUM_FLOATING_POINT) - .mapTo { if (isFinite()) getFormatted(calculationStorage.getPrecision()) else "" } + .mapTo { if (isFinite()) getFormatted(calculationStorage.precision) else "" } _state.update { copy(output = output) } @@ -185,20 +187,23 @@ class CalculatorViewModel( } } - private fun currentBaseChanged(newBase: String, input: String) = viewModelScope.launchIgnored { - Logger.d { "CalculatorViewModel currentBaseChanged $newBase" } - data.conversion = null - _state.update { - copy( - base = newBase, - input = input, - symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() - ) - } + private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = + viewModelScope.launchIgnored { + data.conversion = null + calculationStorage.currentBase = newBase + _state.update { + copy( + base = newBase, + input = input, + symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + ) + } - analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) - analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - } + if (shouldTrack) { + analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) + analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + } + } // region Event override fun onKeyPress(key: String) { @@ -217,10 +222,10 @@ class CalculatorViewModel( } } - override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { + override fun onItemClick(currency: Currency) = with(currency) { Logger.d { "CalculatorViewModel onItemClick ${currency.code}" } - val newInput = currency.rate.toSupportedCharacters().let { + val newInput = rate.toSupportedCharacters().let { if (it.last() == CHAR_DOT) { it.dropLast(1) } else { @@ -229,13 +234,11 @@ class CalculatorViewModel( } _state.update { - copy(input = newInput) + copy( + base = code, + input = newInput + ) } - - @Suppress("MagicNumber") - delay(100) - - calculationStorage.setBase(currency.code) } override fun onItemImageLongClick(currency: Currency) { @@ -247,7 +250,7 @@ class CalculatorViewModel( _effect.emit( CalculatorEffect.ShowConversion( currency.getConversionStringFromBase( - calculationStorage.getBase(), + calculationStorage.currentBase, data.conversion ), currency.code @@ -296,5 +299,11 @@ class CalculatorViewModel( Logger.d { "CalculatorViewModel onSettingsClicked" } _effect.emit(CalculatorEffect.OpenSettings) } + + override fun onBaseChange(base: String) { + Logger.d { "CalculatorViewModel onBaseChange $base" } + currentBaseChanged(base) + calculateOutput(_state.value.input) + } // endregion } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 2c15b82690..9f27b93a25 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -34,7 +34,6 @@ import io.mockative.every import io.mockative.mock import io.mockative.verify import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onSubscription @@ -48,8 +47,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue internal class CalculatorViewModelTest { @@ -98,25 +95,22 @@ internal class CalculatorViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { currencyDataSource.getActiveCurrenciesFlow() } - .returns(flowOf(currencyList)) - - every { calculationStorage.getBaseFlow() } - .returns(flowOf(currency1.code)) + every { calculationStorage.currentBase } + .returns(currency1.code) - runTest { - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + every { calculationStorage.lastInput } + .returns("") - coEvery { calculationStorage.getBase() } - .returns(currency1.code) + every { currencyDataSource.getActiveCurrenciesFlow() } + .returns(flowOf(currencyList)) - coEvery { calculationStorage.getLastInput() } - .returns("") + every { calculationStorage.precision } + .returns(3) - coEvery { calculationStorage.getPrecision() } - .returns(3) + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + runTest { coEvery { currencyDataSource.getActiveCurrencies() } .returns(currencyList) @@ -152,10 +146,10 @@ internal class CalculatorViewModelTest { assertEquals(ConversionState.Online(nowAsDateString()), it.conversionState) assertEquals(currencyList, it.currencyList) assertEquals(shouldShowAds, it.isBannerAdVisible) - assertTrue { it.loading } + assertFalse { it.loading } } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } @@ -163,10 +157,10 @@ internal class CalculatorViewModelTest { fun `init updates the latest base and input`() = runTest { val mock = "mock" - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(currency1.code) - coEvery { calculationStorage.getLastInput() } + every { calculationStorage.lastInput } .returns(mock) viewModel.state.firstOrNull().let { @@ -183,31 +177,6 @@ internal class CalculatorViewModelTest { assertNotNull(viewModel.data.parser) } - @Test - fun `base changes are observed correctly`() = runTest { - coEvery { calculationStorage.getBase() } - .returns(currency1.code) - - coEvery { calculationStorage.getBaseFlow() } - .returns(flowOf(currency1.code)) - - coEvery { backendApiService.getConversion(currency1.code) } - .returns(conversion) - - viewModel.state.firstOrNull().let { - assertNotNull(it) - assertNotNull(viewModel.data.conversion) - assertEquals(currency1.code, viewModel.data.conversion!!.base) - assertEquals(currency1.code, it.base) - - verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } - .wasInvoked() - - verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } - .wasInvoked() - } - } - @Test fun `when api fails and there is conversion in db then conversion rates are calculated`() = runTest { @@ -223,7 +192,7 @@ internal class CalculatorViewModelTest { val result = currencyList.onEach { currency -> currency.rate = conversion.calculateRate(currency.code, it.output) - .getFormatted(calculationStorage.getPrecision()) + .getFormatted(calculationStorage.precision) .toStandardDigits() } @@ -259,29 +228,32 @@ internal class CalculatorViewModelTest { } @Test - fun `when there is few currency app doesn't make API call or search in DB`() = runTest { - every { currencyDataSource.getActiveCurrenciesFlow() } - .returns(flowOf(listOf(currency1))) + fun `when api fails and there is no offline and no enough currency few currency effect emitted`() = + runTest { + coEvery { backendApiService.getConversion(currency1.code) } + .throws(Exception()) - viewModel.effect.onSubscription { - viewModel.event.onKeyPress("1") // trigger api call - }.firstOrNull().let { - assertIs(it) + coEvery { conversionDataSource.getConversionByBase(currency1.code) } + .returns(null) - viewModel.state.value.let { state -> - assertNotNull(state) - assertFalse { state.loading } - assertNull(viewModel.data.conversion) - assertEquals(ConversionState.None, state.conversionState) - } - } + every { currencyDataSource.getActiveCurrenciesFlow() } + .returns(flowOf(listOf(currency1))) - coVerify { conversionDataSource.getConversionByBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.onSubscription { + viewModel.event.onKeyPress("1") // trigger api call + }.firstOrNull().let { + assertIs(it) - coVerify { backendApiService.getConversion(currency1.code) } - .wasNotInvoked() - } + viewModel.state.value.let { state -> + assertNotNull(state) + assertFalse { state.loading } + assertEquals(ConversionState.Error, state.conversionState) + } + } + + coVerify { conversionDataSource.getConversionByBase(currency1.code) } + .wasInvoked() + } @Test fun `when input is too long it should drop the last digit and give TooBigInput effect`() = @@ -369,42 +341,32 @@ internal class CalculatorViewModelTest { var currency = currency1 viewModel.state.onSubscription { viewModel.event.onItemClick(currency1) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency1.code, it.base) assertEquals(currency1.rate, it.input) } - coVerify { calculationStorage.setBase(currency1.code) } - .wasInvoked() - // when last digit is . it should be removed currency = currency.copy(rate = "123.") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123", it.input) } - coVerify { calculationStorage.setBase(currency.code) } - .wasInvoked() currency = currency.copy(rate = "123 456.78") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123456.78", it.input) } - coVerify { calculationStorage.setBase(currency.code) } - .wasInvoked() } @Test @@ -447,7 +409,6 @@ internal class CalculatorViewModelTest { val output = "5" viewModel.effect.onSubscription { viewModel.event.onKeyPress(output) - delay(100) viewModel.event.onOutputLongClick() }.firstOrNull().let { assertEquals(CalculatorEffect.CopyToClipboard(output), it) @@ -538,4 +499,28 @@ internal class CalculatorViewModelTest { assertEquals(key, it.input) } } + + @Test + fun onBaseChanged() = runTest { + every { calculationStorage.currentBase } + .returns(currency1.code) + + coEvery { backendApiService.getConversion(currency1.code) } + .returns(conversion) + + viewModel.state.onSubscription { + viewModel.event.onBaseChange(currency1.code) + }.firstOrNull().let { + assertNotNull(it) + assertNotNull(viewModel.data.conversion) + assertEquals(currency1.code, viewModel.data.conversion!!.base) + assertEquals(currency1.code, it.base) + + verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } + .wasInvoked() + + verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } + .wasInvoked() + } + } } diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt index f64d91f436..e6f2d24700 100644 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt @@ -8,8 +8,8 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CurrenciesState( - val isBannerAdVisible: Boolean = false, - val isOnboardingVisible: Boolean = false, + val isBannerAdVisible: Boolean, + val isOnboardingVisible: Boolean, val currencyList: List = listOf(), val loading: Boolean = true, val selectionVisibility: Boolean = false @@ -30,6 +30,7 @@ sealed class CurrenciesEffect : BaseEffect { data object FewCurrency : CurrenciesEffect() data object OpenCalculator : CurrenciesEffect() data object Back : CurrenciesEffect() + data class ChangeBase(val newBase: String) : CurrenciesEffect() } // Data diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 6c01dae5c4..70d2394247 100755 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch @Suppress("TooManyFunctions") class CurrenciesViewModel( @@ -39,7 +38,12 @@ class CurrenciesViewModel( ) : BaseSEEDViewModel(), CurrenciesEvent { // region SEED - private val _state = MutableStateFlow(CurrenciesState()) + private val _state = MutableStateFlow( + CurrenciesState( + isBannerAdVisible = adControlRepository.shouldShowBannerAd(), + isOnboardingVisible = appStorage.firstRun + ) + ) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -51,14 +55,6 @@ class CurrenciesViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - isOnboardingVisible = appStorage.isFirstRun(), - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } - } currencyDataSource.getCurrenciesFlow() .onEach { currencyList -> @@ -84,10 +80,10 @@ class CurrenciesViewModel( private suspend fun verifyListSize() = data.unFilteredList .filter { it.isActive } .whether { it.size < MINIMUM_ACTIVE_CURRENCY } - ?.whetherNot { appStorage.isFirstRun() } + ?.whetherNot { appStorage.firstRun } ?.run { _effect.emit(CurrenciesEffect.FewCurrency) } - private suspend fun verifyCurrentBase() = calculationStorage.getBase().either( + private suspend fun verifyCurrentBase() = calculationStorage.currentBase.either( { isEmpty() }, { base -> state.value.currencyList @@ -97,10 +93,12 @@ class CurrenciesViewModel( )?.mapTo { state.value.currencyList.firstOrNull { it.isActive }?.code.orEmpty() }?.let { newBase -> - calculationStorage.setBase(newBase) + calculationStorage.currentBase = newBase analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + + _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } private fun filterList(txt: String) = data.unFilteredList @@ -133,7 +131,7 @@ class CurrenciesViewModel( .whether { it < MINIMUM_ACTIVE_CURRENCY } ?.let { _effect.emit(CurrenciesEffect.FewCurrency) } ?: run { - appStorage.setFirstRun(false) + appStorage.firstRun = false _state.update { copy(isOnboardingVisible = false) } filterList("") _effect.emit(CurrenciesEffect.OpenCalculator) diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index 8f7b9ad2a4..bc936f029b 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -14,7 +14,6 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure import io.mockative.every @@ -90,16 +89,14 @@ internal class CurrenciesViewModelTest { every { currencyDataSource.getCurrenciesFlow() } .returns(currencyListFlow) - runTest { - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + every { calculationStorage.currentBase } + .returns(currency1.code) - coEvery { calculationStorage.getBase() } - .returns(currency1.code) - } + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) } // Analytics @@ -149,10 +146,10 @@ internal class CurrenciesViewModelTest { assertFalse { it.loading } } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() } @@ -182,7 +179,7 @@ internal class CurrenciesViewModelTest { @Test fun `don't show FewCurrency effect if there is MINIMUM_ACTIVE_CURRENCY and not firstRun`() = runTest { - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -193,19 +190,18 @@ internal class CurrenciesViewModelTest { } ) - viewModel // init - - coVerify { calculationStorage.setBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.firstOrNull().let { + assertIs(it) + } } @Test fun `don't show FewCurrency effect if there is less than MINIMUM_ACTIVE_CURRENCY it is firstRun`() = runTest { - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(true) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -216,10 +212,9 @@ internal class CurrenciesViewModelTest { } ) - viewModel // init - - coVerify { calculationStorage.setBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.firstOrNull().let { + assertIs(it) + } } @Test @@ -238,14 +233,22 @@ internal class CurrenciesViewModelTest { val firstActiveBase = currency1.code // first active currency every { currencyDataSource.getCurrenciesFlow() } - .returns(flowOf(currencyList)) + .returns( + flow { + delay(1.seconds.inWholeMilliseconds) + emit(currencyList) + } + ) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") - viewModel // init + viewModel.effect.firstOrNull().let { + assertIs(it) + assertEquals(firstActiveBase, it.newBase) + } - coVerify { calculationStorage.setBase(firstActiveBase) } + verify { calculationStorage.currentBase = firstActiveBase } .wasInvoked() } @@ -255,31 +258,41 @@ internal class CurrenciesViewModelTest { currency1 = currency1.copy(isActive = false) // make first item in list not active every { currencyDataSource.getCurrenciesFlow() } - .returns(flowOf(listOf(currency1, currency2, currency3))) + .returns( + flow { + delay(1.seconds.inWholeMilliseconds) + emit(listOf(currency1, currency2, currency3)) + } + ) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(currency1.code) // not active one - viewModel // init + viewModel.effect.firstOrNull().let { + assertIs(it) + assertEquals(currency2.code, it.newBase) + } - coVerify { calculationStorage.setBase(currency2.code) } + verify { calculationStorage.currentBase = currency2.code } .wasInvoked() } // Event @Test - fun updateAllCurrenciesState() = runTest { - coEvery { appStorage.isFirstRun() } + fun updateAllCurrenciesState() { + every { appStorage.firstRun } .returns(false) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("EUR") val mockValue = Random.nextBoolean() viewModel.event.updateAllCurrenciesState(mockValue) - coVerify { currencyDataSource.updateCurrencyStates(mockValue) } - .wasInvoked() + runTest { + coVerify { currencyDataSource.updateCurrencyStates(mockValue) } + .wasInvoked() + } } @Test @@ -392,7 +405,7 @@ internal class CurrenciesViewModelTest { @Test fun onDoneClick() = runTest { - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(true) // where there is single currency @@ -419,7 +432,7 @@ internal class CurrenciesViewModelTest { assertFalse { viewModel.state.value.isOnboardingVisible } - coVerify { appStorage.setFirstRun(false) } + verify { appStorage.firstRun = false } .wasInvoked() } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt index adc2a944ef..b3ae057a6a 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt @@ -8,8 +8,8 @@ import kotlinx.coroutines.Job // State data class MainState( - var shouldOnboardUser: Boolean? = null, - var appTheme: Int? = null + var shouldOnboardUser: Boolean, + var appTheme: Int ) : BaseState // Effect diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index 6dde5b9be5..3aa79de6a7 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -11,7 +11,6 @@ import com.oztechan.ccc.client.core.analytics.model.UserProperty import com.oztechan.ccc.client.core.shared.model.AppTheme import com.oztechan.ccc.client.core.shared.util.isNotPassed import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel -import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository @@ -34,7 +33,12 @@ class MainViewModel( analyticsManager: AnalyticsManager, ) : BaseSEEDViewModel(), MainEvent { // region SEED - private val _state = MutableStateFlow(MainState()) + private val _state = MutableStateFlow( + MainState( + shouldOnboardUser = appStorage.firstRun, + appTheme = appStorage.appTheme + ) + ) override val state: StateFlow = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -46,30 +50,22 @@ class MainViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - appTheme = appStorage.getAppTheme(), - shouldOnboardUser = appStorage.isFirstRun() + with(analyticsManager) { + setUserProperty( + UserProperty.IsPremium( + appStorage.premiumEndDate.isNotPassed().toString() ) - } - with(analyticsManager) { - setUserProperty( - UserProperty.IsPremium( - appStorage.getPremiumEndDate().isNotPassed().toString() - ) - ) - setUserProperty(UserProperty.SessionCount(appStorage.getSessionCount().toString())) - setUserProperty( - UserProperty.AppTheme( - AppTheme.getAnalyticsThemeName( - appStorage.getAppTheme(), - appConfigRepository.getDeviceType() - ) + ) + setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) + setUserProperty( + UserProperty.AppTheme( + AppTheme.getAnalyticsThemeName( + appStorage.appTheme, + appConfigRepository.getDeviceType() ) ) - setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) - } + ) + setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) } } @@ -88,9 +84,9 @@ class MainViewModel( } } - private suspend fun adjustSessionCount() { + private fun adjustSessionCount() { if (data.isNewSession) { - appStorage.setSessionCount(appStorage.getSessionCount() + 1) + appStorage.sessionCount++ data.isNewSession = false } } @@ -109,7 +105,7 @@ class MainViewModel( } } - private suspend fun checkReview() { + private fun checkReview() { if (appConfigRepository.shouldShowAppReview()) { viewModelScope.launch { delay(reviewConfigService.config.appReviewDialogDelay) @@ -125,13 +121,13 @@ class MainViewModel( data.adVisibility = false } - override fun onResume() = viewModelScope.launchIgnored { + override fun onResume() { Logger.d { "MainViewModel onResume" } _state.update { copy( - shouldOnboardUser = appStorage.isFirstRun(), - appTheme = appStorage.getAppTheme() + shouldOnboardUser = appStorage.firstRun, + appTheme = appStorage.appTheme ) } diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index 3fb6fc8a11..cc7e8c79b6 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -21,8 +21,6 @@ import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock @@ -86,54 +84,45 @@ internal class MainViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appConfigRepository.getDeviceType() } - .returns(mockDevice) + every { appStorage.appTheme } + .returns(appThemeValue) - runTest { - coEvery { appStorage.getSessionCount() } - .returns(1L) + every { appStorage.premiumEndDate } + .returns(nowAsLong()) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong()) + every { appStorage.sessionCount } + .returns(1L) - coEvery { adControlRepository.shouldShowInterstitialAd() } - .returns(false) + every { appConfigRepository.getDeviceType() } + .returns(mockDevice) - coEvery { appStorage.isFirstRun() } - .returns(isFirstRun) + every { adControlRepository.shouldShowInterstitialAd() } + .returns(false) - coEvery { appStorage.getAppTheme() } - .returns(appThemeValue) - } + every { appStorage.firstRun } + .returns(isFirstRun) } // Analytics @Test - fun ifUserPropertiesSetCorrect() = runTest { + fun ifUserPropertiesSetCorrect() { viewModel // init - coVerify { + verify { analyticsManager.setUserProperty( UserProperty.IsPremium( - appStorage.getPremiumEndDate().isNotPassed().toString() - ) - ) - }.wasInvoked() - - coVerify { - analyticsManager.setUserProperty( - UserProperty.SessionCount( - appStorage.getSessionCount().toString() + appStorage.premiumEndDate.isNotPassed().toString() ) ) } .wasInvoked() - - coVerify { + verify { analyticsManager.setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) } + .wasInvoked() + verify { analyticsManager.setUserProperty( UserProperty.AppTheme( AppTheme.getAnalyticsThemeName( - appStorage.getAppTheme(), + appStorage.appTheme, mockDevice ) ) @@ -153,10 +142,10 @@ internal class MainViewModelTest { assertEquals(appThemeValue, it.appTheme) } - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() - coVerify { appStorage.getAppTheme() } + verify { appStorage.appTheme } .wasInvoked() } @@ -179,7 +168,7 @@ internal class MainViewModelTest { } @Test - fun `onResume adjustSessionCount`() = runTest { + fun `onResume adjustSessionCount`() = with(viewModel) { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -188,7 +177,7 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } @@ -197,26 +186,26 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(true) } .returns(false) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } .returns("") - assertTrue { viewModel.data.isNewSession } + assertTrue { data.isNewSession } - viewModel.event.onResume() + event.onResume() - coVerify { appStorage.setSessionCount(mockSessionCount + 1) } + verify { appStorage.sessionCount = mockSessionCount + 1 } .wasInvoked() - assertFalse { viewModel.data.isNewSession } + assertFalse { data.isNewSession } - viewModel.event.onResume() + event.onResume() - coVerify { appStorage.setSessionCount(mockSessionCount + 1) } + verify { appStorage.sessionCount = mockSessionCount + 1 } .wasNotInvoked() - assertFalse { viewModel.data.isNewSession } + assertFalse { data.isNewSession } } @Test @@ -229,19 +218,19 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { adControlRepository.shouldShowInterstitialAd() } + every { adControlRepository.shouldShowInterstitialAd() } .returns(true) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -260,39 +249,40 @@ internal class MainViewModelTest { verify { reviewConfigService.config } .wasInvoked() - coVerify { adControlRepository.shouldShowInterstitialAd() } + verify { adControlRepository.shouldShowInterstitialAd() } .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() } @Test - fun `onResume checkAppUpdate nothing happens when check update returns null`() = runTest { - val mockSessionCount = Random.nextLong() + fun `onResume checkAppUpdate nothing happens when check update returns null`() = + with(viewModel) { + val mockSessionCount = Random.nextLong() - every { reviewConfigService.config } - .returns(ReviewConfig(0, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(0, 0L)) - every { adConfigService.config } - .returns(AdConfig(0, 0, 0L, 0L)) + every { adConfigService.config } + .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockSessionCount) + every { appStorage.sessionCount } + .returns(mockSessionCount) - every { appConfigRepository.checkAppUpdate(false) } - .returns(null) + every { appConfigRepository.checkAppUpdate(false) } + .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } - .returns(true) + every { appConfigRepository.shouldShowAppReview() } + .returns(true) - viewModel.event.onResume() + event.onResume() - assertFalse { viewModel.data.isAppUpdateShown } + assertFalse { data.isAppUpdateShown } - verify { appConfigRepository.checkAppUpdate(false) } - .wasInvoked() - } + verify { appConfigRepository.checkAppUpdate(false) } + .wasInvoked() + } @Test fun `onResume checkAppUpdate app review should ask when check update returns not null`() = @@ -300,7 +290,7 @@ internal class MainViewModelTest { val mockSessionCount = Random.nextLong() val mockBoolean = Random.nextBoolean() - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { adConfigService.config } @@ -312,7 +302,7 @@ internal class MainViewModelTest { every { reviewConfigService.config } .returns(ReviewConfig(0, 0L)) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } @@ -344,13 +334,13 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) viewModel.effect.onSubscription { @@ -359,7 +349,7 @@ internal class MainViewModelTest { assertIs(it) } - coVerify { appConfigRepository.shouldShowAppReview() } + verify { appConfigRepository.shouldShowAppReview() } .wasInvoked() verify { reviewConfigService.config } @@ -368,7 +358,7 @@ internal class MainViewModelTest { @Test fun `onResume checkReview should do nothing when shouldShowAppReview returns false`() = - runTest { + with(viewModel) { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -377,18 +367,18 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(false) - viewModel.onResume() + onResume() - coVerify { appConfigRepository.shouldShowAppReview() } + verify { appConfigRepository.shouldShowAppReview() } .wasInvoked() } @@ -397,7 +387,7 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(false) } .returns(false) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { adConfigService.config } @@ -416,10 +406,10 @@ internal class MainViewModelTest { val newAppThemeValue = appThemeValue + 10 val newIsFirstRun = isFirstRun.not() - coEvery { appStorage.getAppTheme() } + every { appStorage.appTheme } .returns(newAppThemeValue) - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(newIsFirstRun) viewModel.state @@ -431,10 +421,10 @@ internal class MainViewModelTest { assertEquals(newAppThemeValue, it.appTheme) } - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() - coVerify { appStorage.getAppTheme() } + verify { appStorage.appTheme } .wasInvoked() } } diff --git a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt index 3812352e7e..23b033f183 100644 --- a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt +++ b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt @@ -44,31 +44,29 @@ class PremiumViewModel( ) = viewModelScope.launchIgnored { Logger.d { "PremiumViewModel onPremiumActivated ${adType?.data?.duration.orEmpty()}" } adType?.let { - appStorage.setPremiumEndDate(it.calculatePremiumEnd(startDate)) + appStorage.premiumEndDate = it.calculatePremiumEnd(startDate) _effect.emit(PremiumEffect.PremiumActivated(it, isRestorePurchase)) } } - override fun onRestorePurchase(oldPurchaseList: List) = - viewModelScope.launchIgnored { - Logger.d { "PremiumViewModel onRestorePurchase" } - val premiumEndDate = appStorage.getPremiumEndDate() - oldPurchaseList - .maxByOrNull { - it.type.calculatePremiumEnd(it.date) - }?.whether( - { type.calculatePremiumEnd(date).isNotPassed() }, - { date > premiumEndDate }, - { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } - )?.run { - onPremiumActivated( - adType = PremiumType.getById(type.data.id), - startDate = this.date, - isRestorePurchase = true - ) - _state.update { copy(loading = false) } - } - } + override fun onRestorePurchase(oldPurchaseList: List) { + Logger.d { "PremiumViewModel onRestorePurchase" } + oldPurchaseList + .maxByOrNull { + it.type.calculatePremiumEnd(it.date) + }?.whether( + { type.calculatePremiumEnd(date).isNotPassed() }, + { date > appStorage.premiumEndDate }, + { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } + )?.run { + onPremiumActivated( + adType = PremiumType.getById(type.data.id), + startDate = this.date, + isRestorePurchase = true + ) + _state.update { copy(loading = false) } + } + } override fun onAddPurchaseMethods(premiumDataList: List) { Logger.d { "PremiumViewModel onAddPurchaseMethods" } diff --git a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt index 49169fb441..d263c3366c 100644 --- a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt +++ b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt @@ -14,10 +14,10 @@ import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType import com.oztechan.ccc.client.viewmodel.premium.util.calculatePremiumEnd import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.configure +import io.mockative.every import io.mockative.mock +import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -72,7 +72,7 @@ internal class PremiumViewModelTest { @Test fun onPremiumActivated() = runTest { viewModel.event.onPremiumActivated(null) - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasNotInvoked() PremiumType.values().forEach { premiumType -> @@ -84,7 +84,7 @@ internal class PremiumViewModelTest { assertEquals(premiumType, it.premiumType) assertFalse { it.isRestorePurchase } - coVerify { appStorage.setPremiumEndDate(premiumType.calculatePremiumEnd(now)) } + verify { appStorage.premiumEndDate = premiumType.calculatePremiumEnd(now) } .wasInvoked() } } @@ -92,7 +92,7 @@ internal class PremiumViewModelTest { @Test fun onRestorePurchase() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) val now = nowAsLong() @@ -109,30 +109,34 @@ internal class PremiumViewModelTest { assertTrue { it.isRestorePurchase } assertFalse { viewModel.state.value.loading } - coVerify { appStorage.setPremiumEndDate(it.premiumType.calculatePremiumEnd(now)) } + verify { appStorage.premiumEndDate = it.premiumType.calculatePremiumEnd(now) } .wasInvoked() } // onRestorePurchase shouldn't do anything if all the old purchases out of dated var oldPurchase = OldPurchase(nowAsLong(), PremiumType.MONTH) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } + verify { + appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) + } .wasNotInvoked() // onRestorePurchase shouldn't do anything if the old purchase is already expired oldPurchase = OldPurchase(nowAsLong() - (32.days.inWholeMilliseconds), PremiumType.MONTH) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } + verify { + appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) + } .wasNotInvoked() } diff --git a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts index 1dc8354b3a..bd79ec9ace 100644 --- a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts +++ b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts @@ -29,10 +29,6 @@ kotlin { implementation(project(currency)) } - Modules.Client.Storage.apply { - implementation(project(calculation)) - } - Modules.Common.Core.apply { implementation(project(model)) } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt index f407f918a8..468d37c91a 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt @@ -20,6 +20,6 @@ interface SelectCurrencyEvent : BaseEvent { // Effect sealed class SelectCurrencyEffect : BaseEffect { - data object DismissDialog : SelectCurrencyEffect() + data class CurrencyChange(val newBase: String) : SelectCurrencyEffect() data object OpenCurrencies : SelectCurrencyEffect() } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt index 4067d25acf..f8e5a2f820 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt @@ -10,7 +10,6 @@ import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource -import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,8 +19,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class SelectCurrencyViewModel( - currencyDataSource: CurrencyDataSource, - private val calculationStorage: CalculationStorage, + currencyDataSource: CurrencyDataSource ) : BaseSEEDViewModel(), SelectCurrencyEvent { // region SEED private val _state = MutableStateFlow(SelectCurrencyState()) @@ -51,8 +49,7 @@ class SelectCurrencyViewModel( // region Event override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { Logger.d { "SelectCurrencyViewModel onItemClick ${currency.code}" } - calculationStorage.setBase(currency.code) - _effect.emit(SelectCurrencyEffect.DismissDialog) + _effect.emit(SelectCurrencyEffect.CurrencyChange(currency.code)) } override fun onSelectClick() = viewModelScope.launchIgnored { diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt index 03e94aad88..44356c362b 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt @@ -5,5 +5,5 @@ import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import org.koin.dsl.module val clientViewModelSelectCurrencyModule = module { - viewModelDefinition { SelectCurrencyViewModel(get(), get()) } + viewModelDefinition { SelectCurrencyViewModel(get()) } } diff --git a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt index 2d01022064..fd18fa17fc 100644 --- a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt +++ b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt @@ -6,10 +6,8 @@ package com.oztechan.ccc.client.viewmodel.selectcurrency import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource -import com.oztechan.ccc.client.storage.calculation.CalculationStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify @@ -33,15 +31,12 @@ import com.oztechan.ccc.common.core.model.Currency as CurrencyCommon internal class SelectCurrencyViewModelTest { private val subject: SelectCurrencyViewModel by lazy { - SelectCurrencyViewModel(currencyDataSource, calculationStorage) + SelectCurrencyViewModel(currencyDataSource) } @Mock private val currencyDataSource = mock(classOf()) - @Mock - private val calculationStorage = mock(classOf()) - private val currencyDollar = CurrencyCommon("USD", "Dollar", "$", "", true) private val currencyEuro = CurrencyCommon("Eur", "Euro", "", "", true) @@ -102,10 +97,8 @@ internal class SelectCurrencyViewModelTest { subject.effect.onSubscription { subject.event.onItemClick(currencyDollar) }.firstOrNull().let { - assertIs(it) - - coVerify { calculationStorage.setBase(currencyDollar.code) } - .wasInvoked() + assertIs(it) + assertEquals(currencyDollar.code, it.newBase) } } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 3a031065e1..67cad2f71a 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -9,7 +9,7 @@ import com.oztechan.ccc.client.viewmodel.settings.model.PremiumStatus // State data class SettingsState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val activeCurrencyCount: Int = 0, val activeWatcherCount: Int = 0, val appThemeType: AppTheme = AppTheme.SYSTEM_DEFAULT, diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index b6ebf556cf..0095ad5d6e 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") class SettingsViewModel( @@ -45,7 +44,8 @@ class SettingsViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), SettingsEvent { // region SEED - private val _state = MutableStateFlow(SettingsState()) + private val _state = + MutableStateFlow(SettingsState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -57,16 +57,13 @@ class SettingsViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.getAppTheme()), - premiumStatus = appStorage.getPremiumEndDate().toPremiumStatus(), - precision = calculationStorage.getPrecision(), - version = appConfigRepository.getVersion(), - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } + _state.update { + copy( + appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), + premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), + precision = calculationStorage.precision, + version = appConfigRepository.getVersion() + ) } currencyDataSource.getActiveCurrenciesFlow() @@ -141,7 +138,7 @@ class SettingsViewModel( override fun onPremiumClick() = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onPremiumClick" } - if (appStorage.getPremiumEndDate().isPassed()) { + if (appStorage.premiumEndDate.isPassed()) { _effect.emit(SettingsEffect.Premium) } else { _effect.emit(SettingsEffect.AlreadyPremium) @@ -170,18 +167,16 @@ class SettingsViewModel( _effect.emit(SettingsEffect.SelectPrecision) } - override fun onPrecisionSelect(index: Int) = viewModelScope.launchIgnored { + override fun onPrecisionSelect(index: Int) { Logger.d { "SettingsViewModel onPrecisionSelect $index" } - index.indexToNumber().let { - calculationStorage.setPrecision(it) - _state.update { copy(precision = it) } - } + calculationStorage.precision = index.indexToNumber() + _state.update { copy(precision = index.indexToNumber()) } } override fun onThemeChange(theme: AppTheme) = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onThemeChange $theme" } _state.update { copy(appThemeType = theme) } - appStorage.setAppTheme(theme.themeValue) + appStorage.appTheme = theme.themeValue _effect.emit(SettingsEffect.ChangeTheme(theme.themeValue)) } // endregion diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index af709a90be..f0dbb832d2 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -116,31 +116,29 @@ internal class SettingsViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) + every { appStorage.appTheme } + .returns(-1) + + every { appStorage.premiumEndDate } + .returns(0) + + every { calculationStorage.precision } + .returns(mockedPrecision) + every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherLists)) + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + every { appConfigRepository.getDeviceType() } .returns(Device.IOS) every { appConfigRepository.getVersion() } .returns(version) - - runTest { - coEvery { appStorage.getPremiumEndDate() } - .returns(0) - - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - - coEvery { calculationStorage.getPrecision() } - .returns(mockedPrecision) - - coEvery { appStorage.getAppTheme() } - .returns(-1) - } } // init @@ -157,7 +155,7 @@ internal class SettingsViewModelTest { assertIs(it.premiumStatus) } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } @@ -169,7 +167,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is never set PremiumStatus is NeverActivated`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) viewModel.state.firstOrNull().let { @@ -180,7 +178,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is passed PremiumStatus is Expired`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -191,7 +189,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is not passed PremiumStatus is Active`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -331,10 +329,10 @@ internal class SettingsViewModelTest { assertIs(it) } - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -343,7 +341,7 @@ internal class SettingsViewModelTest { assertIs(it) } - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() } @@ -399,7 +397,7 @@ internal class SettingsViewModelTest { println("-----") - coVerify { calculationStorage.setPrecision(value.indexToNumber()) } + verify { calculationStorage.precision = value.indexToNumber() } .wasInvoked() } } @@ -416,7 +414,7 @@ internal class SettingsViewModelTest { assertEquals(mockTheme.themeValue, it.themeValue) } - coVerify { appStorage.setAppTheme(mockTheme.themeValue) } + verify { appStorage.appTheme = mockTheme.themeValue } .wasInvoked() } } diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt index f5799306ed..02845f1d61 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt @@ -7,7 +7,7 @@ import com.oztechan.ccc.client.core.viewmodel.BaseState import com.oztechan.ccc.common.core.model.Watcher data class WatchersState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val watcherList: List = emptyList() ) : BaseState diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt index 7b8fc7c910..78f3998f34 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt @@ -29,7 +29,8 @@ class WatchersViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), WatchersEvent { // region SEED - private val _state = MutableStateFlow(WatchersState()) + private val _state = + MutableStateFlow(WatchersState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -41,13 +42,6 @@ class WatchersViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } - } watcherDataSource.getWatchersFlow() .onEach { _state.update { copy(watcherList = it) } @@ -76,11 +70,10 @@ class WatchersViewModel( watcherDataSource.updateWatcherBaseById(newBase, watcher.id) } - override fun onTargetChanged(watcher: Watcher, newTarget: String) = - viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } - watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) - } + override fun onTargetChanged(watcher: Watcher, newTarget: String) = viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } + watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) + } override fun onAddClick() = viewModelScope.launchIgnored { Logger.d { "WatcherViewModel onAddClick" } @@ -102,11 +95,10 @@ class WatchersViewModel( watcherDataSource.deleteWatcher(watcher.id) } - override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = - viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } - watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) - } + override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } + watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) + } override fun onRateChange(watcher: Watcher, rate: String): String { Logger.d { "WatcherViewModel onRateChange $watcher $rate" } diff --git a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt index 66369f1fd6..f980545d38 100644 --- a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt +++ b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt @@ -73,10 +73,8 @@ internal class WatchersViewModelTest { every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherList)) - runTest { - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - } + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) } // init @@ -88,7 +86,7 @@ internal class WatchersViewModelTest { assertEquals(watcherList, it.watcherList) } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd0546fd61..89daaeeb3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,6 @@ common-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl common-testAnnotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } common-koinCore = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } common-multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } -common-multiplatformSettingsCoroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } common-kotlinXDateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinXDateTime" } common-ktorLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } common-ktorJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } diff --git a/ios/CCC/UI/Calculator/CalculatorRootView.swift b/ios/CCC/UI/Calculator/CalculatorRootView.swift index 5c91afaf0e..a90d357d79 100644 --- a/ios/CCC/UI/Calculator/CalculatorRootView.swift +++ b/ios/CCC/UI/Calculator/CalculatorRootView.swift @@ -62,7 +62,7 @@ struct CalculatorRootView: View { text: String(\.choose_at_least_two_currency), buttonText: String(\.select), buttonAction: { - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) } ) } @@ -80,7 +80,10 @@ struct CalculatorRootView: View { .sheet( isPresented: $isBarShown, content: { - SelectCurrencyRootView(isBarShown: $isBarShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $isBarShown, + onCurrencySelected: { observable.event.onBaseChange(base: $0) } + ).environmentObject(navigationStack) } ) .onAppear { @@ -105,7 +108,7 @@ struct CalculatorRootView: View { case is CalculatorEffect.OpenBar: isBarShown = true case is CalculatorEffect.OpenSettings: - navigationStack.push(SettingsRootView()) + navigationStack.push(SettingsRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) case is CalculatorEffect.ShowPasteRequest: isPasteRequestSnackShown.toggle() case let copyToClipboardEffect as CalculatorEffect.CopyToClipboard: diff --git a/ios/CCC/UI/Currencies/CurrenciesRootView.swift b/ios/CCC/UI/Currencies/CurrenciesRootView.swift index f78f7e3bff..71e6e6e156 100644 --- a/ios/CCC/UI/Currencies/CurrenciesRootView.swift +++ b/ios/CCC/UI/Currencies/CurrenciesRootView.swift @@ -24,6 +24,8 @@ struct CurrenciesRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onBaseChange: (String) -> Void + var body: some View { CurrenciesView( event: observable.event, @@ -48,6 +50,8 @@ struct CurrenciesRootView: View { navigationStack.push(CalculatorRootView()) case is CurrenciesEffect.Back: navigationStack.pop() + case let changeBaseEffect as CurrenciesEffect.ChangeBase: + onBaseChange(changeBaseEffect.newBase) default: logger.i(message: { "CurrenciesRootView unknown effect" }) } diff --git a/ios/CCC/UI/Main/MainView.swift b/ios/CCC/UI/Main/MainView.swift index 955ad4a2b2..dbac5a7b80 100644 --- a/ios/CCC/UI/Main/MainView.swift +++ b/ios/CCC/UI/Main/MainView.swift @@ -18,13 +18,10 @@ struct MainView: View { transitionType: .default, easing: Animation.easeInOut ) { - switch state.shouldOnboardUser { - case true: + if state.shouldOnboardUser { IntroSlideRootView() - case false: + } else { CalculatorRootView() - default: - EmptyView() } } } diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift index 209512be66..7523992979 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift @@ -24,6 +24,8 @@ struct SelectCurrencyRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onCurrencySelected: (String) -> Void + var body: some View { SelectCurrencyView( event: observable.event, @@ -40,10 +42,11 @@ struct SelectCurrencyRootView: View { private func onEffect(effect: SelectCurrencyEffect) { logger.i(message: { "SelectCurrencyRootView onEffect \(effect.description)" }) switch effect { - case is SelectCurrencyEffect.DismissDialog: + case let currencyChangeEffect as SelectCurrencyEffect.CurrencyChange: + onCurrencySelected(currencyChangeEffect.newBase) isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: onCurrencySelected)) default: logger.i(message: { "SelectCurrencyRootView unknown effect" }) } diff --git a/ios/CCC/UI/Settings/SettingsRootView.swift b/ios/CCC/UI/Settings/SettingsRootView.swift index 29199dd007..42427f48a7 100644 --- a/ios/CCC/UI/Settings/SettingsRootView.swift +++ b/ios/CCC/UI/Settings/SettingsRootView.swift @@ -31,6 +31,8 @@ struct SettingsRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onBaseChange: ((String) -> Void) + var body: some View { SettingsView( event: observable.event, @@ -72,7 +74,7 @@ struct SettingsRootView: View { case is SettingsEffect.Back: navigationStack.pop() case is SettingsEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: onBaseChange)) case is SettingsEffect.OpenWatchers: navigationStack.push(WatchersRootView()) case is SettingsEffect.FeedBack: diff --git a/ios/CCC/UI/Slides/BugReportSlideRootView.swift b/ios/CCC/UI/Slides/BugReportSlideRootView.swift index e4ba1d9f65..593e24c6e9 100644 --- a/ios/CCC/UI/Slides/BugReportSlideRootView.swift +++ b/ios/CCC/UI/Slides/BugReportSlideRootView.swift @@ -24,7 +24,7 @@ struct BugReportSlideRootView: View { subTitle1: String(\.slide_bug_report_text_1), subTitle2: String(\.slide_bug_report_text_2), buttonText: String(\.got_it), - buttonAction: { navigationStack.push(CurrenciesRootView()) } + buttonAction: { navigationStack.push(CurrenciesRootView(onBaseChange: { _ in })) } ) .onAppear { analyticsManager.trackScreen(screenName: ScreenName.Slider(position: 2)) diff --git a/ios/CCC/UI/Watchers/WatcherItem.swift b/ios/CCC/UI/Watchers/WatcherItem.swift index d938bc3f7f..22324b5fcb 100644 --- a/ios/CCC/UI/Watchers/WatcherItem.swift +++ b/ios/CCC/UI/Watchers/WatcherItem.swift @@ -16,6 +16,9 @@ struct WatcherItem: View { @State private var relationSelection = 0 @State private var amount = "" + @Binding var isBaseBarShown: Bool + @Binding var isTargetBarShown: Bool + let watcher: Provider.Watcher let event: WatchersEvent diff --git a/ios/CCC/UI/Watchers/WatchersRootView.swift b/ios/CCC/UI/Watchers/WatchersRootView.swift index 8dc046a903..a8983c2f84 100644 --- a/ios/CCC/UI/Watchers/WatchersRootView.swift +++ b/ios/CCC/UI/Watchers/WatchersRootView.swift @@ -49,13 +49,29 @@ struct WatchersRootView: View { .sheet( isPresented: $baseBarInfo.isShown, content: { - SelectCurrencyRootView(isBarShown: $baseBarInfo.isShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $baseBarInfo.isShown, + onCurrencySelected: { + observable.event.onBaseChanged( + watcher: baseBarInfo.watcher!, + newBase: $0 + ) + } + ).environmentObject(navigationStack) } ) .sheet( isPresented: $targetBarInfo.isShown, content: { - SelectCurrencyRootView(isBarShown: $targetBarInfo.isShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $targetBarInfo.isShown, + onCurrencySelected: { + observable.event.onTargetChanged( + watcher: targetBarInfo.watcher!, + newTarget: $0 + ) + } + ).environmentObject(navigationStack) } ) .onAppear { diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 4fb2b0420d..61bde8024e 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -33,6 +33,8 @@ struct WatchersView: View { Form { List(state.watcherList, id: \.id) { watcher in WatcherItem( + isBaseBarShown: $baseBarInfo.isShown, + isTargetBarShown: $targetBarInfo.isShown, watcher: watcher, event: event )