From 44a9f2293030f385dab4d525c3cd270a3bc65a62 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Feb 2025 17:48:21 +0200 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../presentation/state/fee/custom/BitcoinCustomFeeConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index 99865323ca..2b7aa838e5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -35,7 +35,7 @@ internal class BitcoinCustomFeeConverter( val feeValue = value.amount.value val feeCurrency = feeCryptoCurrencyStatusProvider()?.value val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value - return if (network != null && (isUseBitcoinFeeConverter(network))) { + return if (network != null && isUseBitcoinFeeConverter(network)) { persistentListOf( SendTextField.CustomFee( value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), From 7026daf2fd761492d784968f7ad5d3723018d491 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Feb 2025 18:03:25 +0300 Subject: [PATCH 2/4] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 4 - .../tap/di/domain/TxHistoryDomainModule.kt | 10 +-- .../tap/proxy/redux/DaggerGraphState.kt | 4 +- .../tangem/tap/routing/utils/ChildFactory.kt | 26 +++++- .../com/tangem/common/routing/AppRoute.kt | 12 +-- features/staking/api/build.gradle.kts | 8 ++ .../features/staking/api/StakingComponent.kt | 18 ++++ .../staking/api/navigation/StakingRouter.kt | 8 -- features/staking/impl/build.gradle.kts | 1 + .../staking/impl/DefaultStakingComponent.kt | 33 +++++++ .../features/staking/impl/di/StakingModule.kt | 38 ++++++++ .../staking/impl/di/StakingRouterModule.kt | 28 ------ .../impl/navigation/DefaultStakingRouter.kt | 8 +- .../impl/navigation/InnerStakingRouter.kt | 3 +- .../impl/presentation/StakingFragment.kt | 77 ----------------- .../StakingClickIntents.kt | 2 +- .../StakingModel.kt} | 86 +++++++++---------- .../impl/presentation/state/StakingUiState.kt | 2 +- .../state/stub/StakingClickIntentsStub.kt | 2 +- .../transformers/SetAmountDataTransformer.kt | 2 +- .../SetInitialDataStateTransformer.kt | 2 +- .../ui/StakingClaimRewardsValidatorContent.kt | 2 +- .../ui/StakingConfirmationContent.kt | 2 +- .../ui/StakingInitialInfoContent.kt | 2 +- .../ui/StakingValidatorListContent.kt | 2 +- 25 files changed, 177 insertions(+), 205 deletions(-) create mode 100644 features/staking/api/src/main/kotlin/com/tangem/features/staking/api/StakingComponent.kt delete mode 100644 features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingModule.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/{viewmodel => model}/StakingClickIntents.kt (96%) rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/{viewmodel/StakingViewModel.kt => model/StakingModel.kt} (95%) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 0fcd5636c2..0e5b92b3ea 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -65,7 +65,6 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.send.api.navigation.SendRouter -import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.google.GoogleServicesHelper @@ -178,9 +177,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var emailSender: EmailSender - @Inject - lateinit var stakingRouter: StakingRouter - @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt index 7f0c9abe88..98eb94e42e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di.domain -import com.tangem.domain.tokens.* import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase @@ -9,27 +8,23 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object TxHistoryDomainModule { @Provides - @ViewModelScoped fun provideGetTxHistoryItemsCountUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsCountUseCase { return GetTxHistoryItemsCountUseCase(repository = txHistoryRepository) } @Provides - @ViewModelScoped fun provideGetTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsUseCase { return GetTxHistoryItemsUseCase(repository = txHistoryRepository) } @Provides - @ViewModelScoped fun providesGetExplorerTransactionUrlUseCase( txHistoryRepository: TxHistoryRepository, ): GetExplorerTransactionUrlUseCase { @@ -37,7 +32,6 @@ internal object TxHistoryDomainModule { } @Provides - @ViewModelScoped fun providesGetFixedTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetFixedTxHistoryItemsUseCase { return GetFixedTxHistoryItemsUseCase(repository = txHistoryRepository) } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 2333f95cf5..3c36d9833b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,7 +1,6 @@ package com.tangem.tap.proxy.redux import com.tangem.blockchainsdk.BlockchainSDKFactory -import com.tangem.data.card.TransactionSignerFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.ui.UiMessageSender @@ -9,6 +8,7 @@ import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -34,7 +34,6 @@ import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter -import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -65,7 +64,6 @@ data class DaggerGraphState( val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null, val getCardInfoUseCase: GetCardInfoUseCase? = null, val issuersConfigStorage: IssuersConfigStorage? = null, - val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 47c916c3b6..70338653b9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -16,7 +16,7 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter -import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.api.StakingComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -58,11 +58,11 @@ internal class ChildFactory @Inject constructor( private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val welcomeComponentFactory: WelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, + private val stakingComponentFactory: StakingComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, private val qrScanningRouter: QrScanningRouter, - private val stakingRouter: StakingRouter, private val testerRouter: TesterRouter, private val pushNotificationRouter: PushNotificationsRouter, private val routingFeatureToggles: RoutingFeatureToggles, @@ -211,6 +211,17 @@ internal class ChildFactory @Inject constructor( componentFactory = storiesComponentFactory, ) } + is AppRoute.Staking -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = StakingComponent.Params( + userWalletId = route.userWalletId, + cryptoCurrencyId = route.cryptoCurrencyId, + yield = route.yield, + ), + componentFactory = stakingComponentFactory, + ) + } is AppRoute.AccessCodeRecovery, is AppRoute.AppCurrencySelector, is AppRoute.SaveWallet, @@ -230,7 +241,6 @@ internal class ChildFactory @Inject constructor( is AppRoute.Wallet, is AppRoute.WalletConnectSessions, is AppRoute.CurrencyDetails, - is AppRoute.Staking, is AppRoute.PushNotification, -> error("Unsupported route: $route") } @@ -337,7 +347,15 @@ internal class ChildFactory @Inject constructor( Child.LegacyIntent(testerRouter.getEntryIntent()) } is AppRoute.Staking -> { - route.asFragmentChild(Provider { stakingRouter.getEntryFragment() }) + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = StakingComponent.Params( + userWalletId = route.userWalletId, + cryptoCurrencyId = route.cryptoCurrencyId, + yield = route.yield, + ), + componentFactory = stakingComponentFactory, + ) } is AppRoute.PushNotification -> { route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() }) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 28d839feba..f2c6c70148 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -257,17 +257,7 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, val yield: Yield, - ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}"), - RouteBundleParams { - - override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val USER_WALLET_ID_KEY = "userWalletId" - const val CRYPTO_CURRENCY_ID_KEY = "cryptoCurrencyId" - const val YIELD_KEY = "yield" - } - } + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}") @Serializable data object PushNotification : AppRoute(path = "/push_notification") diff --git a/features/staking/api/build.gradle.kts b/features/staking/api/build.gradle.kts index a14e2d42b0..b2266699a2 100644 --- a/features/staking/api/build.gradle.kts +++ b/features/staking/api/build.gradle.kts @@ -10,6 +10,14 @@ android { } dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /** AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/StakingComponent.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/StakingComponent.kt new file mode 100644 index 0000000000..207a7bdc94 --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/StakingComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface StakingComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrencyId: CryptoCurrency.ID, + val yield: Yield, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt deleted file mode 100644 index d2b216513e..0000000000 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.features.staking.api.navigation - -import androidx.fragment.app.Fragment - -interface StakingRouter { - - fun getEntryFragment(): Fragment -} \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 6c77d65471..be3dbdd1e2 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.decompose) /** Domain */ diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt new file mode 100644 index 0000000000..3f40fea39d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/DefaultStakingComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.staking.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.staking.api.StakingComponent +import com.tangem.features.staking.impl.presentation.model.StakingModel +import com.tangem.features.staking.impl.presentation.ui.StakingScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultStakingComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: StakingComponent.Params, +) : StakingComponent, AppComponentContext by appComponentContext { + + private val model: StakingModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val currentState by model.uiState.collectAsStateWithLifecycle() + StakingScreen(currentState) + } + + @AssistedFactory + interface Factory : StakingComponent.Factory { + override fun create(context: AppComponentContext, params: StakingComponent.Params): DefaultStakingComponent + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingModule.kt new file mode 100644 index 0000000000..c2fc6404b9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingModule.kt @@ -0,0 +1,38 @@ +package com.tangem.features.staking.impl.di + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.staking.api.StakingComponent +import com.tangem.features.staking.impl.DefaultStakingComponent +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter +import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.model.StakingModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingModule { + + @Binds + fun bindComponentFactory(factory: DefaultStakingComponent.Factory): StakingComponent.Factory + + @Binds + @IntoMap + @ClassKey(StakingModel::class) + fun bindModel(model: StakingModel): Model +} + +@Module +@InstallIn(DecomposeComponent::class) +internal interface StakingComponentModule { + + @Binds + @ComponentScoped + fun bindRouter(impl: DefaultStakingRouter): InnerStakingRouter +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt deleted file mode 100644 index 1e01cf0b3d..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.staking.impl.di - -import com.tangem.common.routing.AppRouter -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.staking.impl.navigation.DefaultStakingRouter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped - -/** - * DI module provides implementation of [StakingRouter] - */ -@Module -@InstallIn(ActivityComponent::class) -internal object StakingRouterModule { - - @Provides - @ActivityScoped - fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter { - return DefaultStakingRouter( - urlOpener = urlOpener, - router = router, - ) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 6acad0658b..9a600a0a93 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,18 +1,18 @@ package com.tangem.features.staking.impl.navigation -import androidx.fragment.app.Fragment import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.staking.impl.presentation.StakingFragment +import javax.inject.Inject -internal class DefaultStakingRouter( +@ComponentScoped +internal class DefaultStakingRouter @Inject constructor( private val urlOpener: UrlOpener, private val router: AppRouter, ) : InnerStakingRouter { - override fun getEntryFragment(): Fragment = StakingFragment.create() override fun openUrl(url: String) { urlOpener.openUrl(url) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index e1fec4bab1..f7d5ab5fa6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -2,9 +2,8 @@ package com.tangem.features.staking.impl.navigation import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.staking.api.navigation.StakingRouter -interface InnerStakingRouter : StakingRouter { +internal interface InnerStakingRouter { fun openUrl(url: String) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt deleted file mode 100644 index 1740d65084..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.features.staking.impl.presentation - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.staking.impl.navigation.InnerStakingRouter -import com.tangem.features.staking.impl.presentation.state.StakingStateController -import com.tangem.features.staking.impl.presentation.state.StakingStateRouter -import com.tangem.features.staking.impl.presentation.ui.StakingScreen -import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -/** - * Staking fragment - */ -@AndroidEntryPoint -internal class StakingFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - @Inject - lateinit var stakingRouter: StakingRouter - - @Inject - lateinit var appRouter: AppRouter - - @Inject - lateinit var stateController: StakingStateController - - @Inject - lateinit var analyticsEventsHandler: AnalyticsEventHandler - - private val viewModel by viewModels() - private val innerStakingRouter: InnerStakingRouter - get() = requireNotNull(stakingRouter as? InnerStakingRouter) { - "innerStakingRouter should be instance of InnerStakingRouter" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycle.addObserver(viewModel) - - viewModel.setRouter( - innerStakingRouter, - StakingStateRouter( - appRouter = appRouter, - stateController = stateController, - analyticsEventsHandler = analyticsEventsHandler, - ), - ) - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - val currentState = viewModel.uiState.collectAsStateWithLifecycle() - StakingScreen(currentState.value) - } - - override fun onDestroy() { - lifecycle.removeObserver(viewModel) - super.onDestroy() - } - - companion object { - /** Create staking fragment instance */ - fun create(): StakingFragment = StakingFragment() - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt similarity index 96% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt index 1b4d3eeee4..19f4af3993 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt @@ -1,4 +1,4 @@ -package com.tangem.features.staking.impl.presentation.viewmodel +package com.tangem.features.staking.impl.presentation.model import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.bottomsheet.permission.state.ApproveType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt similarity index 95% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 1dc4ce0e48..bb2c07e2c4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -1,14 +1,9 @@ -package com.tangem.features.staking.impl.presentation.viewmodel +package com.tangem.features.staking.impl.presentation.model -import android.os.Bundle -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary @@ -17,6 +12,9 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomS import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager @@ -52,6 +50,7 @@ import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender import com.tangem.features.staking.impl.navigation.InnerStakingRouter @@ -77,7 +76,6 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.extensions.orZero -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope @@ -89,11 +87,13 @@ import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") -@HiltViewModel -internal class StakingViewModel @Inject constructor( +@Suppress("LargeClass", "TooManyFunctions", "LongParameterList") +@Stable +@ComponentScoped +internal class StakingModel @Inject constructor( + paramsContainer: ParamsContainer, private val stateController: StakingStateController, - private val dispatchers: CoroutineDispatcherProvider, + override val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, @@ -122,33 +122,30 @@ internal class StakingViewModel @Inject constructor( private val paramsInterceptorHolder: ParamsInterceptorHolder, private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, - savedStateHandle: SavedStateHandle, -) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { + private val innerRouter: InnerStakingRouter, + private val appRouter: AppRouter, +) : Model(), StakingClickIntents { val uiState: StateFlow = stateController.uiState val value: StakingUiState get() = uiState.value - private var stakingStateRouter: StakingStateRouter by Delegates.notNull() + private val params = paramsContainer.require() - private val cryptoCurrencyId: CryptoCurrency.ID = - savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) - ?.unbundle(CryptoCurrency.ID.serializer()) - ?: error("This screen can't be opened without `CryptoCurrency.ID`") + private var stakingStateRouter: StakingStateRouter = StakingStateRouter( + appRouter = appRouter, + stateController = stateController, + analyticsEventsHandler = analyticsEventHandler, + ) - private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Staking.USER_WALLET_ID_KEY) - ?.unbundle(UserWalletId.serializer()) - ?: error("This screen can't be opened without `UserWalletId`") - - private val yield: Yield = savedStateHandle.get(AppRoute.Staking.YIELD_KEY) - ?.unbundle(Yield.serializer()) - ?: error("This screen can't be opened without `Yield`") + private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrencyId + private val userWalletId: UserWalletId = params.userWalletId + private val yield: Yield = params.yield private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var processingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var minimumTransactionAmount: EnterAmountBoundary? = null - private var innerRouter: InnerStakingRouter by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() @@ -218,8 +215,8 @@ internal class StakingViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() } - override fun onCleared() { - super.onCleared() + override fun onDestroy() { + super.onDestroy() paramsInterceptorHolder.removeParamsInterceptor(StakingParamsInterceptor.ID) approvalJobHolder.cancel() feeJobHolder.cancel() @@ -266,7 +263,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrency = cryptoCurrencyStatus.currency, ), ) - viewModelScope.launch { + modelScope.launch { feeLoader.getFee( onStakingFee = { gasEstimate -> stateController.update( @@ -303,7 +300,7 @@ internal class StakingViewModel @Inject constructor( override fun onActionClick() { if (isAssentState()) { - viewModelScope.launch { + modelScope.launch { stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value) stateController.update(SetConfirmationStateInProgressTransformer()) transactionSender.constructAndSendTransactions( @@ -546,7 +543,7 @@ internal class StakingViewModel @Inject constructor( } override fun onApprovalClick() { - viewModelScope.launch { + modelScope.launch { stateController.update( SetApprovalBottomSheetInProgressTransformer { stateController.update(DismissBottomSheetStateTransformer) @@ -619,7 +616,7 @@ internal class StakingViewModel @Inject constructor( } private fun updateNotifications(feeError: GetFeeError? = null) { - viewModelScope.launch { + modelScope.launch { val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data val feeState = confirmationState?.feeState as? FeeState.Content val amountState = value.amountState as? AmountState.Data @@ -733,7 +730,7 @@ internal class StakingViewModel @Inject constructor( private fun awaitForAllowance() { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( - scope = viewModelScope, + scope = modelScope, task = PeriodicTask( delay = ALLOWANCE_UPDATE_DELAY, task = { @@ -783,7 +780,7 @@ internal class StakingViewModel @Inject constructor( } override fun onFailedTxEmailClick(errorMessage: String) { - viewModelScope.launch { + modelScope.launch { val network = cryptoCurrencyStatus.currency.network val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } @@ -822,11 +819,6 @@ internal class StakingViewModel @Inject constructor( innerRouter.openTokenDetails(userWalletId, cryptoCurrency) } - fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { - innerRouter = router - this.stakingStateRouter = stateRouter - } - private suspend fun setupApprovalNeeded() { stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).fold( ifRight = { approval -> @@ -903,7 +895,7 @@ internal class StakingViewModel @Inject constructor( ) } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) } private fun subscribeOnBalanceHiding() { @@ -914,7 +906,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(transformer = HideBalanceStateTransformer(it.isBalanceHidden)) } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) } private fun subscribeOnSelectedAppCurrency() { @@ -925,7 +917,7 @@ internal class StakingViewModel @Inject constructor( appCurrency = maybeAppCurrency.getOrElse { AppCurrency.Default } } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) } private fun subscribeOnStepChanges() { @@ -951,7 +943,7 @@ internal class StakingViewModel @Inject constructor( } } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) .saveIn(stepChangesJobHolder) } @@ -971,13 +963,13 @@ internal class StakingViewModel @Inject constructor( } } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) } private fun updateInitialData() { stateController.updateAll( SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, + clickIntents = this@StakingModel, yield = yield, isAnyTokenStaked = isAnyTokenStaked, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 604fbe7497..e4bc6089b6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -14,7 +14,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 514a94fc76..86f4d53799 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -6,7 +6,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import java.math.BigDecimal @Suppress("TooManyFunctions") diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index 736d2d3fa2..d33a64975d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 43d427fe49..873626c210 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -23,7 +23,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.lib.crypto.BlockchainUtils.isPolkadot import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 180306b99e..99b5c966bd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.extensions.orZero @Composable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 765480203c..866458c643 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -29,7 +29,7 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents @Suppress("LongParameterList") @Composable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 63d2aacc70..a815b1b370 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -50,7 +50,7 @@ import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceStat import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.extensions.orZero diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 6afc60b7b3..a525deb6df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -31,7 +31,7 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.extensions.orZero /** From 46c19f2a3ce07cee458331f25fad69dcef04d36f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Feb 2025 13:19:28 +0300 Subject: [PATCH 3/4] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 27 ++++- .../feature/swap/DefaultSwapComponent.kt | 54 ++++++++- .../tangem/feature/swap/di/SwapModelModule.kt | 20 ++++ .../SwapViewModel.kt => model/SwapModel.kt} | 103 ++++++++---------- .../SwapNotificationsFactory.kt | 2 +- .../SwapProcessDataState.kt | 2 +- .../feature/swap/presentation/SwapFragment.kt | 82 -------------- .../tangem/feature/swap/ui/StateBuilder.kt | 4 +- 8 files changed, 146 insertions(+), 148 deletions(-) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapModelModule.kt rename features/swap/impl/src/main/java/com/tangem/feature/swap/{viewmodels/SwapViewModel.kt => model/SwapModel.kt} (95%) rename features/swap/impl/src/main/java/com/tangem/feature/swap/{presentation => model}/SwapNotificationsFactory.kt (99%) rename features/swap/impl/src/main/java/com/tangem/feature/swap/{viewmodels => model}/SwapProcessDataState.kt (96%) delete mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 70338653b9..ad15758549 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.stories.api.StoriesComponent -import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent @@ -16,6 +15,7 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.swap.SwapComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -59,6 +59,7 @@ internal class ChildFactory @Inject constructor( private val welcomeComponentFactory: WelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, private val stakingComponentFactory: StakingComponent.Factory, + private val swapComponentFactory: SwapComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -222,6 +223,18 @@ internal class ChildFactory @Inject constructor( componentFactory = stakingComponentFactory, ) } + is AppRoute.Swap -> { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SwapComponent.Params( + currencyFrom = route.currencyFrom, + currencyTo = route.currencyTo, + userWalletId = route.userWalletId, + isInitialReverseOrder = route.isInitialReverseOrder, + ), + componentFactory = swapComponentFactory, + ) + } is AppRoute.AccessCodeRecovery, is AppRoute.AppCurrencySelector, is AppRoute.SaveWallet, @@ -237,7 +250,6 @@ internal class ChildFactory @Inject constructor( is AppRoute.QrScanning, is AppRoute.ReferralProgram, is AppRoute.ResetToFactory, - is AppRoute.Swap, is AppRoute.Wallet, is AppRoute.WalletConnectSessions, is AppRoute.CurrencyDetails, @@ -329,7 +341,16 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { ResetCardFragment() }) } is AppRoute.Swap -> { - route.asFragmentChild(Provider { SwapFragment() }) + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SwapComponent.Params( + currencyFrom = route.currencyFrom, + currencyTo = route.currencyTo, + userWalletId = route.userWalletId, + isInitialReverseOrder = route.isInitialReverseOrder, + ), + componentFactory = swapComponentFactory, + ) } is AppRoute.Wallet -> { route.asFragmentChild(Provider { walletRouter.getEntryFragment() }) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index c98473a08e..65c5f62d82 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -1,8 +1,19 @@ package com.tangem.feature.swap +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.arkivanov.essenty.lifecycle.subscribe +import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.model.SwapModel +import com.tangem.feature.swap.router.SwapNavScreen +import com.tangem.feature.swap.ui.SwapScreen +import com.tangem.feature.swap.ui.SwapSelectTokenScreen +import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.features.swap.SwapComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -14,9 +25,50 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted params: SwapComponent.Params, ) : SwapComponent, AppComponentContext by appComponentContext { + private val model: SwapModel = getOrCreateModel(params) + + init { + lifecycle.subscribe( + onStart = model::onStart, + onStop = model::onStop, + ) + } + @Composable override fun Content(modifier: Modifier) { - TODO("Not yet implemented") + Crossfade( + modifier = Modifier.background(TangemTheme.colors.background.secondary), + targetState = model.currentScreen, + label = "", + ) { screen -> + when (screen) { + SwapNavScreen.PromoStories -> { + val storiesConfig = model.uiState.storiesConfig + if (storiesConfig != null) { + SwapStoriesScreen(config = storiesConfig) + } else { + SwapScreen(stateHolder = model.uiState) + } + } + SwapNavScreen.Main -> SwapScreen(stateHolder = model.uiState) + SwapNavScreen.Success -> { + val successState = model.uiState.successState + if (successState != null) { + SwapSuccessScreen(state = successState, model.uiState.onBackClicked) + } else { + SwapScreen(stateHolder = model.uiState) + } + } + SwapNavScreen.SelectToken -> { + val tokenState = model.uiState.selectTokenState + if (tokenState != null) { + SwapSelectTokenScreen(state = tokenState, onBack = model.uiState.onBackClicked) + } else { + SwapScreen(stateHolder = model.uiState) + } + } + } + } } @AssistedFactory diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapModelModule.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapModelModule.kt new file mode 100644 index 0000000000..911918e2b6 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.swap.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.swap.model.SwapModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface SwapModelModule { + + @Binds + @IntoMap + @ClassKey(SwapModel::class) + fun provideSwapModel(model: SwapModel): Model +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt similarity index 95% rename from features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt rename to features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d3e551ea25..7da70ff432 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1,20 +1,20 @@ -package com.tangem.feature.swap.viewmodels +package com.tangem.feature.swap.model -import android.os.Bundle +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse -import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -53,11 +53,11 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -73,11 +73,13 @@ import kotlin.properties.Delegates typealias SuccessLoadedSwapData = Map -@Suppress("LargeClass", "LongParameterList") -@HiltViewModel -internal class SwapViewModel @Inject constructor( +@Suppress("LongParameterList", "LargeClass") +@Stable +@ComponentScoped +internal class SwapModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, private val blockchainInteractor: BlockchainInteractor, - private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -94,27 +96,16 @@ internal class SwapViewModel @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val featureToggles: SwapFeatureToggles, swapInteractorFactory: SwapInteractor.Factory, - private val savedStateHandle: SavedStateHandle, private val urlOpener: UrlOpener, router: AppRouter, -) : ViewModel(), DefaultLifecycleObserver { +) : Model() { - private val initialCurrencyFrom: CryptoCurrency - get() { - return savedStateHandle.get(AppRoute.Swap.CURRENCY_FROM_KEY) - ?.unbundle(CryptoCurrency.serializer()) - ?: error("no expected parameter CryptoCurrency (from) found") - } + private val params = paramsContainer.require() - private val initialCurrencyTo: CryptoCurrency? = savedStateHandle.get(AppRoute.Swap.CURRENCY_TO_KEY) - ?.unbundle(CryptoCurrency.serializer()) - - private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) - ?.unbundle(UserWalletId.serializer()) - ?: error("no expected parameter UserWalletId found") - - private val isInitiallyReversed: Boolean = savedStateHandle.get(AppRoute.Swap.IS_INITIAL_REVERSE_ORDER) - ?: false + private val initialCurrencyFrom = params.currencyFrom + private val initialCurrencyTo = params.currencyTo + private val userWalletId = params.userWalletId + private val isInitiallyReversed = params.isInitialReverseOrder private val swapInteractor = swapInteractorFactory.create(userWalletId) @@ -171,13 +162,13 @@ internal class SwapViewModel @Inject constructor( get() = swapRouter.currentScreen init { - viewModelScope.launch { + modelScope.launch { if (featureToggles.isPromoStoriesEnabled) { initStories() swapRouter.openScreen(SwapNavScreen.PromoStories) } } - viewModelScope.launch(dispatchers.io) { + modelScope.launch(dispatchers.io) { val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() } val wallet = getUserWalletUseCase(userWalletId).getOrNull() @@ -191,32 +182,28 @@ internal class SwapViewModel @Inject constructor( initTokens(isInitiallyReversed) } } - } - override fun onCreate(owner: LifecycleOwner) { - getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach { - isBalanceHidden = it.isBalanceHidden - withContext(dispatchers.main) { + analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) + + getBalanceHidingSettingsUseCase() + .onEach { + isBalanceHidden = it.isBalanceHidden uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } - }.launchIn(viewModelScope) + .launchIn(modelScope) } - override fun onStart(owner: LifecycleOwner) { + fun onStart() { startLoadingQuotesFromLastState(true) } - override fun onStop(owner: LifecycleOwner) { + fun onStop() { singleTaskScheduler.cancelTask() } - override fun onCleared() { + override fun onDestroy() { singleTaskScheduler.cancelTask() - super.onCleared() - } - - fun onScreenOpened() { - analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) + super.onDestroy() } private fun sendSelectTokenScreenOpenedEvent() { @@ -227,7 +214,7 @@ internal class SwapViewModel @Inject constructor( } private fun initTokens(isReverseFromTo: Boolean) { - viewModelScope.launch(dispatchers.main) { + modelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.getTokensDataState(initialCurrencyFrom) }.onSuccess { state -> @@ -286,7 +273,7 @@ internal class SwapViewModel @Inject constructor( } private fun initStories() { - viewModelScope.launch { + modelScope.launch { getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id).fold( ifLeft = { Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}") @@ -361,7 +348,7 @@ internal class SwapViewModel @Inject constructor( ) } singleTaskScheduler.scheduleTask( - viewModelScope, + modelScope, loadQuotesTask( fromToken = fromToken, toToken = toToken, @@ -602,7 +589,7 @@ internal class SwapViewModel @Inject constructor( makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return } - viewModelScope.launch(dispatchers.main) { + modelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.onSwap( swapProvider = provider, @@ -691,7 +678,7 @@ internal class SwapViewModel @Inject constructor( } private fun givePermissionsToSwap() { - viewModelScope.launch(dispatchers.main) { + modelScope.launch(dispatchers.main) { runCatching { val fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" @@ -756,7 +743,7 @@ internal class SwapViewModel @Inject constructor( } private fun onSearchEntered(searchQuery: String) { - viewModelScope.launch(dispatchers.io) { + modelScope.launch(dispatchers.io) { val tokenDataState = dataState.tokensDataState ?: return@launch val group = if (isOrderReversed) { tokenDataState.fromGroup @@ -892,12 +879,12 @@ internal class SwapViewModel @Inject constructor( startLoadingQuotesFromLastState(isSilent = true) } .flowOn(dispatchers.main) - .launchIn(viewModelScope) + .launchIn(modelScope) .saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder) } private fun onChangeCardsClicked() { - viewModelScope.launch { + modelScope.launch { val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency @@ -943,7 +930,7 @@ internal class SwapViewModel @Inject constructor( forceQuotesUpdate: Boolean = false, reduceBalanceBy: BigDecimal = BigDecimal.ZERO, ) { - viewModelScope.launch { + modelScope.launch { val fromToken = dataState.fromCryptoCurrency val toToken = dataState.toCryptoCurrency if (fromToken != null) { @@ -968,7 +955,7 @@ internal class SwapViewModel @Inject constructor( isAmountChangedByUser = true } - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY, forceUpdate = forceQuotesUpdate) { + amountDebouncer.debounce(modelScope, DEBOUNCE_AMOUNT_DELAY, forceUpdate = forceQuotesUpdate) { startLoadingQuotes( fromToken = fromToken, toToken = toToken, @@ -1085,7 +1072,7 @@ internal class SwapViewModel @Inject constructor( onSelectFeeType = { uiState = stateBuilder.dismissBottomSheet(uiState) dataState = dataState.copy(selectedFee = it) - viewModelScope.launch(dispatchers.io) { + modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } }, @@ -1145,7 +1132,7 @@ internal class SwapViewModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) }, onStoriesClose = { - viewModelScope.launch { shouldShowStoriesUseCase.neverToShow(StoryContentIds.STORY_FIRST_TIME_SWAP.id) } + modelScope.launch { shouldShowStoriesUseCase.neverToShow(StoryContentIds.STORY_FIRST_TIME_SWAP.id) } swapRouter.openScreen(SwapNavScreen.Main) }, ) @@ -1227,7 +1214,7 @@ internal class SwapViewModel @Inject constructor( return getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }.stateIn( - scope = viewModelScope, + scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default, ) @@ -1295,7 +1282,7 @@ internal class SwapViewModel @Inject constructor( private fun updateWalletBalance() { dataState.fromCryptoCurrency?.currency?.network?.let { network -> - viewModelScope.launch { + modelScope.launch { withContext(NonCancellable) { updateForBalance(userWalletId, network) } @@ -1336,7 +1323,7 @@ internal class SwapViewModel @Inject constructor( } private fun onFailedTxEmailClick(errorMessage: String) { - viewModelScope.launch { + modelScope.launch { val transaction = dataState.swapDataModel?.transaction val fromCurrencyStatus = dataState.fromCryptoCurrency ?: initialFromStatus val network = fromCurrencyStatus.currency.network diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt similarity index 99% rename from features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt rename to features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 28f045bb1e..249fdce71d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.presentation +package com.tangem.feature.swap.model import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt similarity index 96% rename from features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt rename to features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index 3d53f18f52..86a8a33195 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.viewmodels +package com.tangem.feature.swap.model import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt deleted file mode 100644 index 58316f8c11..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.feature.swap.presentation - -import android.os.Bundle -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels -import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.feature.swap.router.SwapNavScreen -import com.tangem.feature.swap.ui.SwapScreen -import com.tangem.feature.swap.ui.SwapSelectTokenScreen -import com.tangem.feature.swap.ui.SwapSuccessScreen -import com.tangem.feature.swap.viewmodels.SwapViewModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class SwapFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - private val viewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycle.addObserver(viewModel) - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - viewModel.onScreenOpened() - ScreenContent(viewModel = viewModel) - } - - @Suppress("TopLevelComposableFunctions") - @Composable - private fun ScreenContent(viewModel: SwapViewModel) { - Crossfade( - modifier = Modifier.background(TangemTheme.colors.background.secondary), - targetState = viewModel.currentScreen, - label = "", - ) { screen -> - when (screen) { - SwapNavScreen.PromoStories -> { - val storiesConfig = viewModel.uiState.storiesConfig - if (storiesConfig != null) { - SwapStoriesScreen(config = storiesConfig) - } else { - SwapScreen(stateHolder = viewModel.uiState) - } - } - SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState) - SwapNavScreen.Success -> { - val successState = viewModel.uiState.successState - if (successState != null) { - SwapSuccessScreen(state = successState, viewModel.uiState.onBackClicked) - } else { - SwapScreen(stateHolder = viewModel.uiState) - } - } - SwapNavScreen.SelectToken -> { - val tokenState = viewModel.uiState.selectTokenState - if (tokenState != null) { - SwapSelectTokenScreen(state = tokenState, onBack = viewModel.uiState.onBackClicked) - } else { - SwapScreen(stateHolder = viewModel.uiState) - } - } - } - } - } - - override fun onDestroy() { - lifecycle.removeObserver(viewModel) - super.onDestroy() - } -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 3e7157f83e..c586e0bf5a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -33,9 +33,9 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R -import com.tangem.feature.swap.presentation.SwapNotificationsFactory +import com.tangem.feature.swap.model.SwapNotificationsFactory import com.tangem.feature.swap.utils.formatToUIRepresentation -import com.tangem.feature.swap.viewmodels.SwapProcessDataState +import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.PERCENT From 850ecba894e40f83d42cbb8f4f061e1a52844a8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Feb 2025 12:43:00 +0400 Subject: [PATCH 4/4] Updated on 2026-08-14 --- .../tangem/datasource/di/QuotesStoreModule.kt | 14 ++- .../local/datastore/RuntimeSharedStore.kt | 68 ++++++++++++ .../local/quote/DefaultQuotesStore.kt | 100 +++++++++++------- .../datasource/local/quote/QuotesStore.kt | 4 + .../local/quote/converter/QuoteConverter.kt | 29 +++++ .../datasource/local/quote/model/QuoteDM.kt | 14 --- .../local/quote/utils/QuotesSerializer.kt | 32 ------ .../utils/MoshiDataStoreSerializer.kt | 46 ++++++++ .../tangem/datasource/utils/MoshiTypesExt.kt | 16 +++ .../converters/HotCryptoCurrencyConverter.kt | 1 + .../com/tangem/domain/tokens/model/Quote.kt | 2 + .../tangem/domain/tokens/mock/MockQuotes.kt | 10 ++ 12 files changed, 251 insertions(+), 85 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/MoshiDataStoreSerializer.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index 7d45e11efc..dbcf34e870 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -4,9 +4,12 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.quote.DefaultQuotesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.quote.utils.QuotesSerializer +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -29,11 +32,16 @@ internal object QuotesStoreModule { dispatchers: CoroutineDispatcherProvider, ): QuotesStore { return DefaultQuotesStore( - dataStore = DataStoreFactory.create( - serializer = QuotesSerializer(moshi), + persistenceStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), + ), produceFile = { context.dataStoreFile(fileName = "quotes") }, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ), + runtimeStore = RuntimeSharedStore(), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt new file mode 100644 index 0000000000..1636a37cd0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt @@ -0,0 +1,68 @@ +package com.tangem.datasource.local.datastore + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull + +/** + * Runtime shared store + * +[REDACTED_AUTHOR] + */ +interface RuntimeSharedStore { + + /** Get flow of elements [T] */ + fun get(): Flow + + /** Get element [T] synchronously or null */ + suspend fun getSyncOrNull(): T? + + /** Get element [T] synchronously or default value */ + suspend fun getSyncOrDefault(default: T): T + + /** Store [value] */ + suspend fun store(value: T) + + /** + * Update + * + * @param default default value if store is empty + * @param function update function + */ + suspend fun update(default: T, function: (T) -> T) + + companion object { + + /** + * Create [RuntimeSharedStore] + * + * @param T type of stored value + */ + operator fun invoke(): RuntimeSharedStore = object : RuntimeSharedStore { + + private val flow = MutableSharedFlow(replay = 1) + + init { + flow.tryEmit(value = null) + } + + override fun get(): Flow = flow.mapNotNull { it ?: return@mapNotNull null } + + override suspend fun getSyncOrNull(): T? = flow.firstOrNull() + + override suspend fun getSyncOrDefault(default: T): T = getSyncOrNull() ?: default + + override suspend fun store(value: T) { + flow.emit(value = value) + } + + override suspend fun update(default: T, function: (T) -> T) { + val storedData = flow.firstOrNull() ?: default + val updatedData = function(storedData) + + flow.emit(value = updatedData) + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 3bad4c835a..150f67eb66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -2,54 +2,82 @@ package com.tangem.datasource.local.quote import androidx.datastore.core.DataStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.quote.model.QuoteDM -import com.tangem.datasource.local.quote.model.QuotesDM +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.quote.converter.QuoteConverter import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.extensions.orZero -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +internal typealias QuotesByCurrencyId = Map + +/** + * Default implementation of [QuotesStore] + * + * @property persistenceStore persistence quotes store + * @property runtimeStore runtime quotes store + */ internal class DefaultQuotesStore( - private val dataStore: DataStore, + private val persistenceStore: DataStore, + private val runtimeStore: RuntimeSharedStore>, ) : QuotesStore { - override fun get(currenciesIds: Set): Flow> { - return dataStore.data - .map { quotes -> createQuotes(currenciesIds, quotes) } + override fun get(currenciesIds: Set): Flow> = channelFlow { + val cachedQuotes = getCachedQuotes(currenciesIds = currenciesIds) + + if (cachedQuotes.isNotEmpty()) { + send(cachedQuotes) + } + + runtimeStore.get() + .onEach { + val mergedQuotes = mergeQuotes(cachedQuotes = cachedQuotes, runtimeQuotes = it) + send(element = mergedQuotes) + } + .launchIn(scope = this) } override suspend fun getSync(currenciesIds: Set): Set { - val quotes = dataStore.data.firstOrNull().orEmpty() - - return createQuotes(currenciesIds, quotes) + return runtimeStore.getSyncOrDefault(default = emptySet()) + .filter { it.rawCurrencyId in currenciesIds } + .toSet() } - private fun createQuotes(currenciesIds: Set, quoteEntities: Set): Set = - currenciesIds.mapTo(mutableSetOf()) { id -> - quoteEntities.firstOrNull { it.rawCurrencyId == id } - ?.let { quote -> - Quote.Value( - rawCurrencyId = id, - fiatRate = quote.fiatRate, - priceChange = quote.priceChange, - ) - } - ?: Quote.Empty(id) - } - override suspend fun store(response: QuotesResponse) { - val newQuotes = response.quotes.mapTo(mutableSetOf()) { (currencyId, quote) -> - QuoteDM( - rawCurrencyId = CryptoCurrency.RawID(currencyId), - fiatRate = quote.price.orZero(), - priceChange = quote.priceChange24h.orZero().movePointLeft(2), - ) - } - - dataStore.updateData { storedQuotes -> - (newQuotes + storedQuotes).distinctBy { it.rawCurrencyId }.toSet() + coroutineScope { + launch { storeInRuntimeStore(response = response) } + launch { storeInPersistenceStore(response = response) } } } + + private suspend fun getCachedQuotes(currenciesIds: Set): Set { + val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet() + val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids } + + return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries) + } + + private fun mergeQuotes(cachedQuotes: Set, runtimeQuotes: Set): Set { + return runtimeQuotes.map { runtimeQuote -> + if (runtimeQuote is Quote.Empty) { + cachedQuotes.firstOrNull { runtimeQuote.rawCurrencyId == it.rawCurrencyId } ?: runtimeQuote + } else { + runtimeQuote + } + } + .toSet() + } + + private suspend fun storeInRuntimeStore(response: QuotesResponse) { + val new = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries) + + runtimeStore.update(default = emptySet()) { saved -> + (saved + new).distinctBy { it.rawCurrencyId }.toSet() + } + } + + private suspend fun storeInPersistenceStore(response: QuotesResponse) { + persistenceStore.updateData { storedQuotes -> storedQuotes + response.quotes } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt index 2d2fada694..feb0872d92 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -5,11 +5,15 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import kotlinx.coroutines.flow.Flow +/** Quotes store */ interface QuotesStore { + /** Get flow of quotes for [currenciesIds] */ fun get(currenciesIds: Set): Flow> + /** Get quotes for [currenciesIds] synchronously */ suspend fun getSync(currenciesIds: Set): Set + /** Store [response] from remote */ suspend fun store(response: QuotesResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt new file mode 100644 index 0000000000..314d2ee920 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.local.quote.converter + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero + +/** + * Converter from [QuotesResponse.Quote] to [Quote.Value] + * + * @property isCached flag that determines whether the quote is a cache + * +[REDACTED_AUTHOR] + */ +internal class QuoteConverter(private val isCached: Boolean) : + Converter, Quote.Value> { + + override fun convert(value: Map.Entry): Quote.Value { + val (currencyId, quote) = value + + return Quote.Value( + rawCurrencyId = CryptoCurrency.RawID(currencyId), + fiatRate = quote.price.orZero(), + priceChange = quote.priceChange24h.orZero().movePointLeft(2), + isCached = isCached, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt deleted file mode 100644 index 1465465631..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/QuoteDM.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.local.quote.model - -import com.squareup.moshi.JsonClass -import com.tangem.domain.tokens.model.CryptoCurrency -import java.math.BigDecimal - -internal typealias QuotesDM = Set - -@JsonClass(generateAdapter = true) -internal data class QuoteDM( - val rawCurrencyId: CryptoCurrency.RawID, - val fiatRate: BigDecimal, - val priceChange: BigDecimal, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt deleted file mode 100644 index 524683ce7d..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/utils/QuotesSerializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.datasource.local.quote.utils - -import androidx.datastore.core.Serializer -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.datasource.local.quote.model.QuoteDM -import com.tangem.datasource.local.quote.model.QuotesDM -import java.io.InputStream -import java.io.OutputStream - -internal class QuotesSerializer(moshi: Moshi) : Serializer { - - private val adapter by lazy { - val types = Types.newParameterizedType(Set::class.java, QuoteDM::class.java) - - moshi.adapter(types) - } - - override val defaultValue: QuotesDM = emptySet() - - override suspend fun readFrom(input: InputStream): QuotesDM { - return input.bufferedReader().use { reader -> - adapter.fromJson(reader.readText()) ?: defaultValue - } - } - - override suspend fun writeTo(t: QuotesDM, output: OutputStream) { - output.bufferedWriter().use { write -> - write.write(adapter.toJson(t)) - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiDataStoreSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiDataStoreSerializer.kt new file mode 100644 index 0000000000..c76773424f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiDataStoreSerializer.kt @@ -0,0 +1,46 @@ +package com.tangem.datasource.utils + +import androidx.datastore.core.Serializer +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.ParameterizedType + +/** + * Moshi serializer [JsonAdapter] for [androidx.datastore.core.DataStore] + * + * @property defaultValue default value + * @property adapter moshi adapter + * +[REDACTED_AUTHOR] + */ +class MoshiDataStoreSerializer( + override val defaultValue: T, + private val adapter: JsonAdapter, +) : Serializer { + + /** + * Constructor + * + * @param moshi moshi for creating adapter + * @param types types of data + * @param defaultValue default value + */ + constructor(moshi: Moshi, types: ParameterizedType, defaultValue: T) : this( + defaultValue = defaultValue, + adapter = moshi.adapter(types), + ) + + override suspend fun readFrom(input: InputStream): T { + return input.bufferedReader().use { reader -> + adapter.fromJson(reader.readText()) ?: defaultValue + } + } + + override suspend fun writeTo(t: T, output: OutputStream) { + output.bufferedWriter().use { write -> + write.write(adapter.toJson(t)) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt new file mode 100644 index 0000000000..2330648ff4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.utils + +import com.squareup.moshi.Types +import java.lang.reflect.ParameterizedType + +inline fun mapWithStringKeyTypes(): ParameterizedType { + return Types.newParameterizedType(Map::class.java, String::class.java, T::class.java) +} + +inline fun listTypes(): ParameterizedType { + return Types.newParameterizedType(List::class.java, T::class.java) +} + +inline fun setTypes(): ParameterizedType { + return Types.newParameterizedType(Set::class.java, T::class.java) +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index a810cc5105..4d16a5b159 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -94,6 +94,7 @@ internal class HotCryptoCurrencyConverter( rawCurrencyId = rawCurrencyId, fiatRate = fiatRate, priceChange = priceChange.movePointLeft(2), + isCached = false, // It doesn't matter ) } else { Quote.Empty(rawCurrencyId) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index c330160b97..d4d7fb06c7 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -19,10 +19,12 @@ sealed interface Quote { * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. + * @property isCached flag that determines whether the quote is a cache */ data class Value( override val rawCurrencyId: CryptoCurrency.RawID, val fiatRate: BigDecimal, val priceChange: BigDecimal, + val isCached: Boolean, ) : Quote } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 5dde109c87..a3b1ee5456 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -12,60 +12,70 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), + isCached = false, ) val quote2 = Quote.Value( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), + isCached = false, ) val quote3 = Quote.Value( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), + isCached = false, ) val quote4 = Quote.Value( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), + isCached = false, ) val quote5 = Quote.Value( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), + isCached = false, ) val quote6 = Quote.Value( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), + isCached = false, ) val quote7 = Quote.Value( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), + isCached = false, ) val quote8 = Quote.Value( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), + isCached = false, ) val quote9 = Quote.Value( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), + isCached = false, ) val quote10 = Quote.Value( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), + isCached = false, ) val quote11 = Quote.Empty(CryptoCurrency.RawID("null"))