diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ff4810cfc6..21ab576e7f 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -76,7 +76,7 @@ interface ApplicationEntryPoint { fun getBalanceHidingRepository(): BalanceHidingRepository - fun getUserTokensStore(): UserTokensStore + fun getAppPreferencesStore(): AppPreferencesStore fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 42280203ef..ef8490029a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val balanceHidingRepository: BalanceHidingRepository get() = entryPoint.getBalanceHidingRepository() - private val userTokensStore: UserTokensStore - get() = entryPoint.getUserTokensStore() + private val appPreferencesStore: AppPreferencesStore + get() = entryPoint.getAppPreferencesStore() val getAppThemeModeUseCase: GetAppThemeModeUseCase get() = entryPoint.getGetAppThemeModeUseCase() @@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { } derivationsFinder = DerivationsFinder( - newTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, dispatchers = AppCoroutineDispatcherProvider(), ) appStateHolder.mainStore = store diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 28b30cb9e3..031784429c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,10 +1,8 @@ package com.tangem.tap.di.domain -import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.GetTokenMarketInfoUseCase -import com.tangem.domain.markets.GetTokenPriceChartUseCase -import com.tangem.domain.markets.GetTokenQuotesUseCase +import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.repository.QuotesRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -37,7 +35,13 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase { - return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository) + fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase { + return GetTokenFullQuotesUseCase(marketsTokenRepository = marketsTokenRepository) + } + + @Provides + @Singleton + fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase { + return GetTokenQuotesUseCase(quotesRepository = quotesRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index a714d71200..a4b7ff17e4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -145,18 +145,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideIsStakeMoreAvailableUseCase( - stakingRepository: StakingRepository, - stakingErrorResolver: StakingErrorResolver, - ): IsStakeMoreAvailableUseCase { - return IsStakeMoreAvailableUseCase( - stakingRepository = stakingRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideIsApproveNeededUseCase( diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 24c222149b..6e5da90b47 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -158,8 +158,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchCurrencyStatusUseCase { - return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index d1f7b8a3d6..6fcad66476 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO @@ -22,7 +25,7 @@ internal data class BlockchainToDerive( // FIXME: May be move to DI, currently unnecessary internal class DerivationsFinder( - private val newTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -64,7 +67,9 @@ internal class DerivationsFinder( } private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = newTokensStore.getSyncOrNull(userWalletId) + val responseTokens = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?.tokens ?: return hashSetOf() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index b7b5b15da2..06e802b5e0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory @@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { title = dialog.title.resolveReference(), message = dialog.description.resolveReference(), isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = dialog.confirmText.resolveReference(), warning = true, onClick = dialog.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 41b568b7ed..59af4ac24b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview @@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { title = dialog.title.resolveReference(), selectedItemIndex = dialog.selectedItemIndex, items = dialog.items.map { it.resolveReference() }.toImmutableList(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = dialog.onDismiss, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 7f972b948d..4abe4b37cc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder @@ -173,13 +174,7 @@ internal class CardSettingsViewModel @Inject constructor( userWalletId = userWalletId, cardId = card.cardId, isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = when (val status = card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - CardDTO.BackupStatus.NoBackup, - null, - -> 0 - }, + backupCardsCount = scanResponse.getBackupCardsCount() ?: 0, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 1b52a50fd1..6f35968117 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { BasicDialog( title = stringResource(dialog.titleResId), message = stringResource(dialog.messageResId), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = dialog.onDismiss, ), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.card_settings_action_sheet_reset), warning = true, onClick = dialog.onConfirmClick, @@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) { BasicDialog( title = stringResource(id = dialog.titleResId), message = stringResource(id = dialog.messageResId), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = dialog.onConfirmClick, ), diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 508c8a2f4e..292b7b1bf6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { viewModel.onSearchClick() } else { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } store.dispatch(TokensAction.SetArgs.ReadAccess) } }, diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index c545f2f83b..0299b8832d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } } private fun scanCard() { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 115bd8d273..1f3b36e57d 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Composable @@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) { BasicDialog( title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title), message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_enable), onClick = dialog.onEnroll, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( onClick = dialog.onCancel, ), onDismissDialog = dialog.onCancel, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index a6315d5b1e..57f226a9d7 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) { }, ), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) { title = stringResource(id = R.string.common_attention), message = stringResource(id = R.string.key_invalidated_warning_description), onDismissDialog = warning.onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), @@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) { message = stringResource(id = R.string.biometric_unavailable_warning), onDismissDialog = warning.onDismiss, isDismissable = false, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = warning.onDismiss, ), 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 af1bebc286..c2b8cf249c 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 @@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.ManageTokensToggles import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter @@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor( private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, + private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -126,13 +128,7 @@ internal class ChildFactory @Inject constructor( if (manageTokensToggles.isFeatureEnabled) { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params( - mode = if (route.readOnlyContent) { - ManageTokensComponent.Mode.READ_ONLY - } else { - ManageTokensComponent.Mode.MANAGE - }, - ), + params = ManageTokensComponent.Params(route.userWalletId), componentFactory = manageTokensComponentFactory, ) } else { @@ -191,6 +187,16 @@ internal class ChildFactory @Inject constructor( componentFactory = walletSettingsComponentFactory, ) } + is AppRoute.MarketsTokenDetails -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = MarketsTokenDetailsComponent.Params( + token = route.token, + appCurrency = route.appCurrency, + ), + componentFactory = marketsTokenDetailsComponentFactory, + ) + } } } diff --git a/app/src/main/res/layout/layout_receipt_total.xml b/app/src/main/res/layout/layout_receipt_total.xml deleted file mode 100644 index 5ff5d3377c..0000000000 --- a/app/src/main/res/layout/layout_receipt_total.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index a492ede7aa..71933a08b2 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.staking.models) + implementation(projects.domain.markets.models) + implementation(projects.domain.appCurrency.models) /* Libs - Other */ api(deps.kotlin.serialization) 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 6c03778967..bae031de6d 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 @@ -5,6 +5,8 @@ import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -177,8 +179,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( - val readOnlyContent: Boolean, - ) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams { + val userWalletId: UserWalletId? = null, + ) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } @@ -262,4 +264,10 @@ sealed class AppRoute(val path: String) : Route { data class WalletSettings( val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") + + @Serializable + data class MarketsTokenDetails( + val token: TokenMarketParams, + val appCurrency: AppCurrency, + ) : AppRoute(path = "/markets_token_details/${token.id}") } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt new file mode 100644 index 0000000000..0761de97a5 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/RouterProxy.kt @@ -0,0 +1,51 @@ +package com.tangem.common.routing.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.decompose.navigation.Router +import kotlin.reflect.KClass + +/** + * Temporary solution to convert [AppRouter] to [Router]. + + * (through manual ComponentContext creation). + * + * **Will be removed when all screens will be migrated to Decompose.** + * + * @return [Router] that wraps [AppRouter]. + */ +fun AppRouter.asRouter(): Router { + return RouterProxy(appRouter = this) +} + +private class RouterProxy( + private val appRouter: AppRouter, +) : Router { + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + if (route is AppRoute) { + appRouter.push(route, onComplete) + } + } + + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + routes.filterIsInstance().let { + appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete) + } + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + appRouter.pop(onComplete) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + if (route is AppRoute) { + appRouter.popTo(route, onComplete) + } + } + + @Suppress("UNCHECKED_CAST") + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + appRouter.popTo(routeClass as KClass, onComplete) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt index e51a8f72a6..8eeec1a691 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt @@ -22,6 +22,11 @@ class PriceAndTimePointValuesConverter( private val formatYValuesCache = mutableMapOf() private val formatXValuesCache = mutableMapOf() + private data class Point( + val x: BigDecimal, + val y: BigDecimal, + ) + override fun convert(data: MarketChartData.Data): MarketChartRawData { formatYValuesCache.clear() formatXValuesCache.clear() @@ -33,8 +38,10 @@ class PriceAndTimePointValuesConverter( ) minMaxCache = cache - val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY) - val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX) + val points = data.x.zip(data.y) { x, y -> Point(x, y) }.sortedBy { it.x } + + val normY = points.map { it.y }.normalizeToDouble(min = cache.minY, max = cache.maxY) + val normX = points.map { it.x }.normalizeTime(min = cache.minX, max = cache.maxX) return if (normX.size > MAX_POINTS) { LTThreeBuckets diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index e57cd07bd1..2d6b9e12d6 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.coil) /** Deps */ implementation(deps.kotlin.immutable.collections) @@ -30,6 +31,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.transaction.models) + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..2cb03f4f16 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/SendTransactionAlertConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.utils.converter.Converter + +class SendTransactionAlertConverter( + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) : Converter { + override fun convert(value: SendTransactionError): AlertUM? { + return when (value) { + is SendTransactionError.DemoCardError -> AlertDemoModeUM( + onConfirmClick = popBackStack, + ) + is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = null, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), + onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> AlertTransactionErrorUM( + code = "", + cause = value.message, + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> AlertTransactionErrorUM( + code = value.code.orEmpty(), + cause = value.message.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> AlertTransactionErrorUM( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt new file mode 100644 index 0000000000..e63ccb0229 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt @@ -0,0 +1,13 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference + +data class AlertDemoModeUM( + override val onConfirmClick: () -> Unit, +) : AlertUM { + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt new file mode 100644 index 0000000000..5fd83163be --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt @@ -0,0 +1,21 @@ +package com.tangem.common.ui.alerts.models + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList + +data class AlertTransactionErrorUM( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit)? = null, +) : AlertUM { + override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) + override val message: TextReference = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt new file mode 100644 index 0000000000..1cf955bebe --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt @@ -0,0 +1,12 @@ +package com.tangem.common.ui.alerts.models + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +interface AlertUM { + val title: TextReference? + val message: TextReference + val confirmButtonText: TextReference + val onConfirmClick: (() -> Unit)? +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index d45e7c07bd..528d288023 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState @@ -81,6 +82,10 @@ class AmountFieldChangeTransformer( cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 73c62d311c..f10c86f973 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -59,7 +59,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { BasicDialog( message = content.data.dialogText.resolveReference(), title = stringResource(id = R.string.common_approve), - confirmButton = DialogButton { isPermissionAlertShow = false }, + confirmButton = DialogButtonUM { isPermissionAlertShow = false }, onDismissDialog = {}, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 8a9ccffd1e..92a791ad8a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -7,8 +7,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,18 +50,10 @@ fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifi @Composable private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { + val wrappedButton by rememberNavigationButton(primaryButton) AnimatedContent( - targetState = primaryButton, - transitionSpec = { - val isPrimaryToHide = targetState != null && initialState == null - val isPrimaryWasVisible = targetState == null && initialState != null - if (isPrimaryToHide || isPrimaryWasVisible) { - slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()) - .togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut())) - } else { - fadeIn().togetherWith(fadeOut()) - } - }, + targetState = wrappedButton, + transitionSpec = { navigationButtonsTransition() }, contentAlignment = Alignment.Center, label = "Animate show primary button", modifier = modifier.fillMaxWidth(), @@ -90,18 +81,10 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = @Composable private fun SecondaryButton(secondaryButton: NavigationButton?) { + val wrappedButton by rememberNavigationButton(secondaryButton) AnimatedContent( - targetState = secondaryButton, - transitionSpec = { - val isPrimaryToHide = targetState != null && initialState == null - val isPrimaryWasVisible = targetState == null && initialState != null - if (isPrimaryToHide || isPrimaryWasVisible) { - slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()) - .togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut())) - } else { - fadeIn().togetherWith(fadeOut()) - } - }, + targetState = wrappedButton, + transitionSpec = { navigationButtonsTransition() }, contentAlignment = Alignment.Center, label = "Animate show secondary button", modifier = Modifier.fillMaxWidth(), @@ -182,6 +165,28 @@ private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: } } +@Composable +private fun rememberNavigationButton(button: NavigationButton?): MutableState { + return remember( + button?.iconRes, + button?.isIconVisible, + button?.isEnabled, + button?.showProgress, + button?.textReference, + ) { mutableStateOf(button) } +} + +private fun AnimatedContentTransitionScope.navigationButtonsTransition(): ContentTransform { + val isPrimaryToHide = targetState != null && initialState == null + val isPrimaryWasVisible = targetState == null && initialState != null + return if (isPrimaryToHide || isPrimaryWasVisible) { + slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()) + .togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut())) + } else { + fadeIn().togetherWith(fadeOut()) + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt new file mode 100644 index 0000000000..91f837f146 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -0,0 +1,213 @@ +package com.tangem.common.ui.userwallet + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CardColors +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.common.ui.R +import com.tangem.core.ui.coil.RotationTransformation +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun UserWalletItem( + state: UserWalletItemUM, + modifier: Modifier = Modifier, + blockColors: CardColors = TangemBlockCardColors, +) { + BlockCard( + modifier = modifier, + colors = blockColors, + onClick = state.onClick, + enabled = state.isEnabled, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size68) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + CardImage(imageUrl = state.imageUrl) + NameAndInfo( + modifier = Modifier.weight(1f), + name = state.name, + information = state.information, + ) + + when (state.endIcon) { + UserWalletItemUM.EndIcon.None -> {} + UserWalletItemUM.EndIcon.Arrow -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.EndIcon.Checkmark -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { + Column( + modifier = modifier.heightIn(min = TangemTheme.dimens.size40), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = name.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + AnimatedContent( + targetState = information.resolveReference(), + label = "User wallet information", + ) { information -> + Text( + text = information, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) { + val imageModifier = modifier + .width(TangemTheme.dimens.size24) + .height(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCornersSmall) + + SubcomposeAsyncImage( + modifier = imageModifier, + model = ImageRequest.Builder(LocalContext.current) + .transformations(RotationTransformation(angle = 90f)) + .size( + width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, + height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, + ) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { + RectangleShimmer( + modifier = imageModifier, + radius = TangemTheme.dimens.size2, + ) + }, + error = { + Image( + modifier = imageModifier, + imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36), + contentDescription = null, + ) + }, + contentDescription = null, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + val list = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("My Wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Old wallet"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = true, + onClick = {}, + endIcon = UserWalletItemUM.EndIcon.Arrow, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = getInformation(3, "4 496,75 $"), + imageUrl = "", + isEnabled = false, + endIcon = UserWalletItemUM.EndIcon.Checkmark, + onClick = {}, + ), + ) + + Column( + Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + list.fastForEach { userWalletItemUM -> + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + state = userWalletItemUM, + ) + } + } + } +} + +private fun getInformation(cardCount: Int, totalBalance: String): TextReference { + val t1 = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val divider = stringReference(value = " • ") + val t2 = stringReference(totalBalance) + + return TextReference.Combined(wrappedList(t1, divider, t2)) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt new file mode 100644 index 0000000000..f8e361a6f2 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.userwallet.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.wallets.models.UserWalletId +import javax.annotation.concurrent.Immutable + +@Immutable +data class UserWalletItemUM( + val id: UserWalletId, + val name: TextReference, + val information: TextReference, + val imageUrl: String, + val isEnabled: Boolean, + val endIcon: EndIcon = EndIcon.None, + val onClick: () -> Unit, +) { + enum class EndIcon { + None, + Arrow, + Checkmark, + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 2d5bd551ac..1c9b1ab3e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -34,7 +34,7 @@ interface StakeKitApi { @POST("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> @POST("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index d6c09517c1..9063ff0b74 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -48,7 +48,7 @@ data class ActionRequestBodyArgs( @Json(name = "ledgerWalletAPICompatible") val ledgerWalletAPICompatible: Boolean? = null, @Json(name = "tronResource") - val tronResource: String? = null, + val tronResource: TronResource? = null, @Json(name = "signatureVerification") val signatureVerification: SignatureVerification? = null, @Json(name = "inputToken") @@ -60,4 +60,12 @@ data class SignatureVerification( val message: String, @Json(name = "signed") val signed: String, -) \ No newline at end of file +) + +enum class TronResource { + @Json(name = "ENERGY") + ENERGY, + + @Json(name = "BANDWIDTH") + BANDWIDTH, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt new file mode 100644 index 0000000000..f12f5fe5ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/tron/TronStakeKitTransaction.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction.tron + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TronStakeKitTransaction( + @Json(name = "raw_data_hex") + val rawDataHex: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt deleted file mode 100644 index b2997661fb..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.token.AppPreferencesUserTokensStore -import com.tangem.datasource.local.token.UserTokensStore -import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object UserTokensStoreModule { - - @Provides - @Singleton - fun provideUserTokensStore( - appPreferencesStore: AppPreferencesStore, - userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, - ): UserTokensStore { - return AppPreferencesUserTokensStore( - appPreferencesStore = appPreferencesStore, - userTokensStoreMigrationRunner = userTokensStoreMigrationRunner, - userWalletsStore = userWalletsStore, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt deleted file mode 100644 index 80d70825bf..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* - -/** - * Implementation of [UserTokensStore] that based on [appPreferencesStore] - * - * @property appPreferencesStore application preference store - * -[REDACTED_AUTHOR] - */ -internal class AppPreferencesUserTokensStore( - private val appPreferencesStore: AppPreferencesStore, - private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, - private val userWalletsStore: UserWalletsStore, - private val dispatchers: CoroutineDispatcherProvider, -) : UserTokensStore { - - init { - runUserTokensMigrations() - } - - override fun get(key: UserWalletId): Flow { - return appPreferencesStore - .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) - .filterNotNull() - } - - override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? { - return appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - ) - } - - override suspend fun store(key: UserWalletId, value: UserTokensResponse) { - appPreferencesStore.storeObject( - key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), - value = value, - ) - } - - // TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA] - private fun runUserTokensMigrations() { - userWalletsStore.userWallets - .filter { it.isNotEmpty() } - .take(1) - .onEach { userWallets -> - userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue }) - } - .flowOn(dispatchers.io) - .launchIn(CoroutineScope(dispatchers.io)) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index c88970b238..a80f1b8e05 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -3,49 +3,53 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock internal class DefaultStakingBalanceStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: StringKeyDataStore>, ) : StakingBalanceStore { - override fun get(): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + private val mutex = Mutex() + + override fun get(userWalletId: UserWalletId): Flow> { + return dataStore.get(userWalletId.stringValue) } - override suspend fun getSyncOrNull(): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { + return dataStore.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(items: List) { - return dataStore.store(STAKING_BALANCE_KEY, items) + override suspend fun store(userWalletId: UserWalletId, items: Set) { + mutex.withLock { + dataStore.store(userWalletId.stringValue, items) + } } - override fun get(integrationId: String): Flow> { - return dataStore.get(STAKING_BALANCE_KEY) + override fun get(userWalletId: UserWalletId, integrationId: String): Flow> { + return dataStore.get(userWalletId.stringValue) .map { balances -> balances.filter { it.integrationId == integrationId } .flatMap { it.balances } } } - override suspend fun getSyncOrNull(integrationId: String): List? { - return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? { + return dataStore.getSyncOrNull(userWalletId.stringValue) ?.firstOrNull { it.integrationId == integrationId }?.balances } - override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { - val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) - ?.toMutableList() - ?.addOrReplace(item) { item.integrationId == integrationId } - ?: listOf(item) + override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) { + mutex.withLock { + val balances = dataStore.getSyncOrNull(userWalletId.stringValue) + ?.addOrReplace(item) { it.integrationId == integrationId } + ?: setOf(item) - return dataStore.store(STAKING_BALANCE_KEY, balances) - } - - companion object { - private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" + dataStore.store(userWalletId.stringValue, balances) + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 0a9ea06c9e..1dc58ba38d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -2,19 +2,20 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface StakingBalanceStore { - fun get(): Flow> + fun get(userWalletId: UserWalletId): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? - suspend fun store(items: List) + suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(integrationId: String): Flow> + fun get(userWalletId: UserWalletId, integrationId: String): Flow> - suspend fun getSyncOrNull(integrationId: String): List? + suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List? - suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) + suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt deleted file mode 100644 index f3f12f026b..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -@Deprecated( - message = "Use AppPreferencesStore", - replaceWith = ReplaceWith( - expression = "AppPreferencesStore", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, -) -interface UserTokensStore { - - @Deprecated( - message = "Use getObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObject(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - fun get(key: UserWalletId): Flow - - @Deprecated( - message = "Use getObjectSyncOrNull", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? - - @Deprecated( - message = "Use storeObject", - replaceWith = ReplaceWith( - expression = "appPreferencesStore.storeObject(userWalletId, response)", - imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), - ), - level = DeprecationLevel.WARNING, - ) - suspend fun store(key: UserWalletId, value: UserTokensResponse) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt deleted file mode 100644 index dae09c0616..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.datasource.local.token - -import androidx.datastore.core.DataMigration -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject - -/** - * Migration of saving [UserTokensResponse] from file to [AppPreferencesStore] - * - * @param userWalletId user wallet id - * @param moshi moshi - * @property fileReader file reader - * -[REDACTED_AUTHOR] - */ -internal class UserTokensStoreMigration( - userWalletId: String, - moshi: Moshi, - private val fileReader: FileReader, -) : DataMigration { - - private val legacyFileName = "user_tokens_$userWalletId" - private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId) - - @OptIn(ExperimentalStdlibApi::class) - private val adapter = moshi.adapter() - - override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true - - override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore { - val currentKey = currentData.getObjectSyncOrNull(key = keyName) - - if (currentKey != null) return currentData - - val value = runCatching { - val json = fileReader.readFile(legacyFileName) - adapter.fromJson(json) - }.getOrNull() - - if (value != null) { - currentData.storeObject(key = keyName, value = value) - } - - return currentData - } - - override suspend fun cleanUp() { - fileReader.removeFile(legacyFileName) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt deleted file mode 100644 index eafb84aa79..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.datasource.local.token - -import com.squareup.moshi.Moshi -import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Runner that launch migrations of saving user tokens store - * - * @property appPreferencesStore application preference store - * @property fileReader file reader - * @property moshi moshi - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -@Singleton -class UserTokensStoreMigrationRunner @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, - private val fileReader: FileReader, - @NetworkMoshi private val moshi: Moshi, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun run(ids: List) { - ids.forEach { id -> - coroutineScope { run(id) } - } - } - - private suspend fun run(id: String) { - withContext(dispatchers.io) { - val migration = UserTokensStoreMigration( - userWalletId = id, - moshi = moshi, - fileReader = fileReader, - ) - - migration.migrate(appPreferencesStore) - - migration.cleanUp() - } - } -} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt index 586779ec4a..6ffebce9fe 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt @@ -19,6 +19,7 @@ class DefaultAppComponentContext( messageHandler: UiMessageHandler, override val dispatchers: CoroutineDispatcherProvider, override val hiltComponentBuilder: DecomposeComponent.Builder, + private val replaceRouter: Router? = null, ) : AppComponentContext, ComponentContext by componentContext { override val tags: HashMap = HashMap() @@ -31,5 +32,5 @@ class DefaultAppComponentContext( get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() } override val router: Router - get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) } + get() = replaceRouter ?: instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) } } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index b1797d3a2a..81ebcec61e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -100,6 +100,7 @@ Aktivieren Aktiviert Fehler + Umtausch Erkunden Transaktionsverlauf einsehen Explorer @@ -834,6 +835,8 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Das Genehmigungsverfahren ist derzeit im Gange und wird in Kürze abgeschlossen sein + Genehmigung läuft Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. @@ -859,6 +862,8 @@ Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden Der zu erhaltende Betrag muss mindestens %s betragen + Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@) + Ausgewähltes Paar vorübergehend nicht verfügbar Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s Der zu tauschende Betrag muss mindestens %s betragen @@ -873,6 +878,8 @@ Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. Geringe Anzahl von Unterschriften Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt. + MATIC wird auf POL migriert. Es gibt jedoch keine Frist, und MATIC wird noch nicht abgeschafft. Du kannst MATIC-Token weiterhin verwenden oder sie über eien Exchange gegen POL tauschen. + Migration von MATIC zu POL Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index cbfddbaa7f..f27b090b8a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -98,6 +98,7 @@ 有効にする 有効 エラー + 交換 移動する 取引履歴を調べる エクスプローラー @@ -822,6 +823,8 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + 許可付与のプロセスは現在進行中であり、まもなく完了する予定です。 + 承認中 カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ff9fce14ea..3d8a825d38 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -683,7 +683,7 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. - Повторный стейкинг + Сменить валидатора Застейкать вознаграждения Отозвать Переголосовать @@ -701,6 +701,7 @@ Стейкинг закрыт Застейкать еще Застейкать %s + Вывести %s Разблокировать Выведено из стейкинга Проверьте процесс завершения стейкинга, чтобы вывести свои средства. @@ -858,8 +859,6 @@ Для работы с сетью необходим депозит Обмен будет доступен после завершения %s транзакции У вас есть активная транзакция - Разрешение обмена в процессе и будет скоро завершено - Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. У вас в списке нет монет доступных для обмена с %s Нет доступных для обмена токенов diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 4a4839b4e7..d0ff9d566d 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -703,7 +703,7 @@ Місяць Тиждень Винагороди - Застейкати + Стейкінг закрито Застейкати більше Застейкати %s Зняти зі стейкінгу %s @@ -843,6 +843,8 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. @@ -860,14 +862,13 @@ Для роботи з мережею вимагається депозит Обмін буде доступний після завершення %s транзакції У вас є активна транзакція - Затвердження обміну триває і незабаром буде завершено - Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. У вашому списку немає доступних монет для обміну %s Немає доступних токенів для обміну Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s Сума отримання не може бути меншою за %s + Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@) Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index eb5bc2de81..3c2329daab 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -63,6 +63,7 @@ OK 主卡片 + %1$s-%2$s 拒絕 重新命名 保存設置 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dadb67073c..329dcfb63e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -114,6 +114,10 @@ Go to provider Go to token Import + + in %d day + in %d days + Later Locked Main network @@ -655,12 +659,12 @@ Market rating Metrics Minimum Requirement - No rewards to claim + No rewards Reward claiming Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it. Reward schedule This is a schedule that determines when participants in staking receive their rewards. - Rewards to claim: %s + Rewards: %s Staking %s Unbonding Period The period you must wait after requesting to withdraw funds from staking before the tokens become available. @@ -668,12 +672,13 @@ The allocated time for activating participation in staking. Migrate Native staking - Staking allow you to earn %1$s. Your staking rewards arrive every day. - Staking allow you to earn %1$s. Your staking rewards arrive every hour. - Staking allow you to earn %1$s. Your staking rewards arrive every month. - Staking allow you to earn %1$s. Your staking rewards arrive every week. + Staking allows you to earn %1$s. Your staking rewards arrive every day. + Staking allows you to earn %1$s. Your staking rewards arrive every hour. + Staking allows you to earn %1$s. Your staking rewards arrive every month. + Staking allows you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards - Rewards stop accruing immediately after you unstake. The unstaking process takes %s. + Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s. + Ready to withdraw Rebond Restake Restake rewards @@ -694,6 +699,7 @@ Stake more Stake %s Unstake %s + Unbonding Unlock locked Unstaked Check unstaked to claim your assets @@ -875,6 +881,8 @@ Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. + MATIC is being migrated to POL. However there is no deadline set and MATIC isn\'t being deprecated yet. You can safely continue using MATIC token or use exchanges to swap it for POL. + MATIC to POL Migration Use your card to get an address for %d network Use your card to get an addresses for %d networks diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index cb8f7f15ee..3781bde624 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.reorderable) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt new file mode 100644 index 0000000000..6a8019af1e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/coil/RotationTransformation.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.coil + +import android.graphics.Bitmap +import android.graphics.Matrix +import coil.size.Size +import coil.transform.Transformation + +class RotationTransformation(private val angle: Float) : Transformation { + + override val cacheKey: String = "rotate:$angle" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val matrix = Matrix().apply { + val centerX = input.width / 2f + val centerY = input.height / 2f + + postRotate(angle, centerX, centerY) + } + + return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 97a3b692cc..e741b087fe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -49,10 +49,10 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun BasicDialog( message: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -72,7 +72,7 @@ fun BasicDialog( fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { TangemDialog( type = DialogType.Message(message), - confirmButton = DialogButton(onClick = onDismissDialog), + confirmButton = DialogButtonUM(onClick = onDismissDialog), onDismissDialog = onDismissDialog, title = null, dismissButton = null, @@ -97,12 +97,12 @@ fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) { @Composable fun TextInputDialog( fieldValue: TextFieldValue, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (TextFieldValue) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -125,12 +125,12 @@ fun TextInputDialog( @Composable fun TextInputDialog( fieldValue: String, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, onValueChange: (String) -> Unit, - textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, + textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() }, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, isDismissable: Boolean = true, ) { TangemDialog( @@ -154,7 +154,7 @@ fun TextInputDialog( fun SelectorDialog( selectedItemIndex: Int, items: ImmutableList, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onSelect: (index: Int) -> Unit, onDismissDialog: () -> Unit, title: String? = null, @@ -180,7 +180,7 @@ fun SelectorDialog( * @param enabled If false button will be disabled * @param onClick Button click callback */ -data class DialogButton( +data class DialogButtonUM( val title: String? = null, val warning: Boolean = false, val enabled: Boolean = true, @@ -190,7 +190,7 @@ data class DialogButton( /** * Additional params for dialog text field */ -data class AdditionalTextInputDialogParams( +data class AdditionalTextInputDialogUM( val label: String? = null, val placeholder: String? = null, val caption: String? = null, @@ -203,10 +203,10 @@ data class AdditionalTextInputDialogParams( @Composable private fun TangemDialog( type: DialogType, - confirmButton: DialogButton, + confirmButton: DialogButtonUM, onDismissDialog: () -> Unit, title: String? = null, - dismissButton: DialogButton? = null, + dismissButton: DialogButtonUM? = null, properties: DialogProperties = DialogProperties(), ) { Dialog(properties = properties, onDismissRequest = onDismissDialog) { @@ -304,7 +304,11 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } @Composable -private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) { +private fun DialogButtons( + confirmButton: DialogButtonUM, + dismissButton: DialogButtonUM?, + modifier: Modifier = Modifier, +) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( @@ -413,13 +417,13 @@ private sealed class DialogType { data class TextInput( val value: TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class SimpleTextInput( val value: String, val onValueChange: (String) -> Unit, - val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), + val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(), ) : DialogType() data class Selector( @@ -445,8 +449,8 @@ private fun BasicDialogPreview() { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -478,8 +482,8 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) { message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " + "password to work with the app", title = "Attention", - confirmButton = DialogButton(warning = true) {}, - dismissButton = DialogButton {}, + confirmButton = DialogButtonUM(warning = true) {}, + dismissButton = DialogButtonUM {}, onDismissDialog = {}, ) } @@ -502,10 +506,10 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) { TextInputDialog( fieldValue = TextFieldValue(text = ""), title = "Rename Wallet", - confirmButton = DialogButton {}, + confirmButton = DialogButtonUM {}, onDismissDialog = {}, onValueChange = {}, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = "Wallet name", ), ) @@ -530,7 +534,7 @@ private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider: title = param.title, items = param.items, selectedItemIndex = param.selectedItemIndex, - confirmButton = DialogButton(title = "Cancel", onClick = {}), + confirmButton = DialogButtonUM(title = "Cancel", onClick = {}), onSelect = {}, onDismissDialog = {}, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 5d09690136..c0f97514dc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -101,7 +102,11 @@ fun TextShimmer( * Height and min width will be set automatically */ @Composable -fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) { +fun SmallButtonShimmer( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + withIcon: Boolean = false, +) { PrimarySmallButton( config = SmallButtonConfig( text = stringReference("B"), @@ -113,7 +118,7 @@ fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) }, ), modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .clip(shape) .shimmer(LocalTangemShimmer.current), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 900aafe401..4565c520d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -25,10 +26,12 @@ import kotlinx.collections.immutable.persistentListOf @Immutable class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope +// TODO: [REDACTED_JIRA] @Composable fun InformationBlock( title: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, + contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, action: (@Composable BoxScope.() -> Unit)? = null, content: (@Composable InformationBlockContentScope.() -> Unit)? = null, ) { @@ -38,15 +41,28 @@ fun InformationBlock( .background(color = TangemTheme.colors.background.action), horizontalAlignment = Alignment.Start, ) { + val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40 + val padding = if (action == null) { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ) + } else { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing11, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing5, + ) + } + Row( modifier = Modifier .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size40) - .padding( - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing6, - ) - .padding(horizontal = TangemTheme.dimens.spacing12), + .heightIn(min = minHeight) + .padding(paddingValues = padding), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -72,7 +88,7 @@ fun InformationBlock( if (content != null) { Box( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(horizontal = contentHorizontalPadding) .fillMaxWidth(), ) { val scope = InformationBlockContentScope(scope = this) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index e02ae97109..195fae0923 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -33,6 +33,7 @@ data class SmallButtonConfig( val text: TextReference, val onClick: () -> Unit, val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, + val enabled: Boolean = true, ) /** @@ -57,6 +58,7 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie SmallButton(config = config, isPrimary = false, modifier = modifier) } +@Suppress("LongMethod") @Composable private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) { val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16) @@ -77,7 +79,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: color = backgroundColor, shape = shape, ) - .clickable(enabled = true, onClick = config.onClick) + .clickable(enabled = config.enabled, onClick = config.onClick) .padding( paddingValues = when (config.icon) { is TangemButtonIconPosition.None -> PaddingValues( @@ -100,7 +102,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: iconPosition = config.icon, text = { val textColor by animateColorAsState( - targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + targetValue = when { + !config.enabled -> TangemTheme.colors.text.disabled + isPrimary -> TangemTheme.colors.text.primary2 + else -> TangemTheme.colors.text.primary1 + }, label = "Update text color", ) @@ -116,7 +122,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Icon( modifier = Modifier.size(TangemTheme.dimens.size16), painter = painterResource(id = iconResId), - tint = TangemTheme.colors.icon.secondary, + tint = if (config.enabled) { + TangemTheme.colors.icon.secondary + } else { + TangemTheme.colors.icon.inactive + }, contentDescription = null, ) }, @@ -174,5 +184,12 @@ private fun ButtonsSample() { icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), ), ) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Add token"), + icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), + enabled = false, + ), + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 11fcaeb099..3033f2221a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -1,5 +1,10 @@ package com.tangem.core.ui.components.buttons.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -36,6 +41,7 @@ fun TangemButton( textStyle: TextStyle = TangemTheme.typography.button, shape: Shape = size.toShape(), iconPadding: Dp = size.toIconPadding(), + animateContentChange: Boolean = false, ) { val multipleClickPreventer = remember { MultipleClickPreventer.get() } @@ -56,6 +62,7 @@ fun TangemButton( buttonIcon = icon, iconPadding = iconPadding, showProgress = showProgress, + animateContentChange = animateContentChange, progressIndicator = { CircularProgressIndicator( modifier = Modifier.buttonContentSize(maxContentSize), @@ -108,24 +115,54 @@ private inline fun RowScope.ButtonContentContainer( buttonIcon: TangemButtonIconPosition, iconPadding: Dp, showProgress: Boolean, + animateContentChange: Boolean, progressIndicator: @Composable RowScope.() -> Unit, - text: @Composable RowScope.() -> Unit, - icon: @Composable RowScope.(Int) -> Unit, + crossinline text: @Composable RowScope.() -> Unit, + crossinline icon: @Composable RowScope.(Int) -> Unit, additionalText: @Composable () -> Unit, ) { if (showProgress) { progressIndicator() } else { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Row(horizontalArrangement = Arrangement.Center) { - if (buttonIcon is TangemButtonIconPosition.Start) { - icon(buttonIcon.iconResId) - Spacer(modifier = Modifier.requiredWidth(iconPadding)) + if (animateContentChange) { + AnimatedContent( + targetState = buttonIcon, + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + label = "button text with icon", + ) { iconState -> + Row(horizontalArrangement = Arrangement.Center) { + when (iconState) { + is TangemButtonIconPosition.Start -> { + icon(iconState.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + text() + } + is TangemButtonIconPosition.End -> { + text() + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(iconState.iconResId) + } + is TangemButtonIconPosition.None -> { + text() + } + } + } } - text() - if (buttonIcon is TangemButtonIconPosition.End) { - Spacer(modifier = Modifier.requiredWidth(iconPadding)) - icon(buttonIcon.iconResId) + } else { + Row(horizontalArrangement = Arrangement.Center) { + if (buttonIcon is TangemButtonIconPosition.Start) { + icon(buttonIcon.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + } + text() + if (buttonIcon is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(buttonIcon.iconResId) + } } } additionalText() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 85e49532fc..f19975ab9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -38,7 +38,7 @@ sealed class CurrencyIconState { * Represents a token icon. * * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property topBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. May be `null`. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. * @property fallbackTint The color to be used for tinting the fallback icon. @@ -46,7 +46,7 @@ sealed class CurrencyIconState { */ data class TokenIcon( val url: String?, - @DrawableRes override val topBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int?, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, val fallbackTint: Color, @@ -81,4 +81,28 @@ sealed class CurrencyIconState { override val showCustomBadge: Boolean = false override val topBadgeIconResId: Int? = null } + + fun copySealed( + isGrayscale: Boolean = this.isGrayscale, + showCustomBadge: Boolean = this.showCustomBadge, + topBadgeIconResId: Int? = this.topBadgeIconResId, + ): CurrencyIconState = when (this) { + is CoinIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + is CustomTokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId ?: this.topBadgeIconResId, + ) + is TokenIcon -> copy( + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + topBadgeIconResId = topBadgeIconResId, + ) + is Loading, + is Locked, + -> this + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt new file mode 100644 index 0000000000..9d5c548b93 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/InfiniteListHandler.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.components.list + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* + +@Composable +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { + val loadMore by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + + lastVisibleItemIndex > totalItemsNumber - buffer + } + } + + val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } + var emitted by remember(totalItemsCount) { mutableStateOf(false) } + + LaunchedEffect(loadMore) { + if (loadMore && !emitted) { + emitted = onLoadMore() + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt index de78fc1368..26aec82eab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt @@ -1,6 +1,21 @@ package com.tangem.core.ui.components.marketprice +import java.math.BigDecimal +import java.math.RoundingMode + /** Price changing type */ enum class PriceChangeType { UP, DOWN, NEUTRAL, + ; + + companion object { + @Suppress("MagicNumber") + fun fromBigDecimal(priceChangePercent: BigDecimal): PriceChangeType { + return when { + priceChangePercent < BigDecimal.ZERO -> DOWN + priceChangePercent.setScale(4, RoundingMode.HALF_UP) > BigDecimal.ZERO -> UP + else -> NEUTRAL + } + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt index 0e594fbd91..67ef1ac47d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.* @@ -68,6 +69,7 @@ private class ChildArrowScope( @Composable fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val figureWidth = TangemTheme.dimens.size40 val strokeColor = TangemTheme.colors.stroke.secondary @@ -86,18 +88,31 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { ) val arrowHeadRectDp = DpRect( origin = DpOffset( - x = figureWidth - arrowHeadSize.width, + x = if (isLtr) { + figureWidth - arrowHeadSize.width + } else { + 0.dp + }, y = figureRectDp.size.center.y - arrowHeadSize.center.y, ), size = arrowHeadSize, ) - val curvedArrowRectDp = DpRect( - top = figureRectDp.top, - left = TangemTheme.dimens.size18, - right = figureRectDp.right - arrowHeadRectDp.width, - bottom = figureRectDp.size.center.y, - ) + val curvedArrowRectDp = if (isLtr) { + DpRect( + top = figureRectDp.top, + left = TangemTheme.dimens.size18, + right = figureRectDp.right - arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } else { + DpRect( + top = figureRectDp.top, + left = arrowHeadRectDp.width, + right = TangemTheme.dimens.size18 + arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + } Canvas( modifier = Modifier @@ -114,20 +129,26 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { drawScope = this, ) - scope.drawCurveArrow() - scope.drawArrowHead() + scope.drawCurveArrow(isLtr) + scope.drawArrowHead(isLtr) if (!isLastChild) { - scope.drawArrowLine() + scope.drawArrowLine(isLtr) } } } -private fun ChildArrowScope.drawArrowHead() { +private fun ChildArrowScope.drawArrowHead(isLtr: Boolean) { val arrowHeadPath = Path().apply { - moveTo(arrowHeadRect.centerRight) - lineTo(arrowHeadRect.topLeft) - lineTo(arrowHeadRect.bottomLeft) + if (isLtr) { + moveTo(arrowHeadRect.centerRight) + lineTo(arrowHeadRect.topLeft) + lineTo(arrowHeadRect.bottomLeft) + } else { + moveTo(arrowHeadRect.centerLeft) + lineTo(arrowHeadRect.topRight) + lineTo(arrowHeadRect.bottomRight) + } close() } val paint = Paint().apply { @@ -143,13 +164,21 @@ private fun ChildArrowScope.drawArrowHead() { } } -private fun ChildArrowScope.drawCurveArrow() { +private fun ChildArrowScope.drawCurveArrow(isLtr: Boolean) { val curveArrowPath = Path().apply { - moveTo(curvedArrowRect.topLeft) - quadraticBezierTo( - control = curvedArrowRect.bottomLeft, - end = curvedArrowRect.bottomRight, - ) + if (isLtr) { + moveTo(curvedArrowRect.topLeft) + quadraticBezierTo( + control = curvedArrowRect.bottomLeft, + end = curvedArrowRect.bottomRight, + ) + } else { + moveTo(curvedArrowRect.topRight) + quadraticBezierTo( + control = curvedArrowRect.bottomRight, + end = curvedArrowRect.bottomLeft, + ) + } } drawPath( path = curveArrowPath, @@ -158,11 +187,20 @@ private fun ChildArrowScope.drawCurveArrow() { ) } -private fun ChildArrowScope.drawArrowLine() { - drawLine( - color = strokeColor, - start = curvedArrowRect.topLeft, - end = Offset(curvedArrowRect.left, figureRect.bottom), - strokeWidth = arrowStrokeWidth, - ) +private fun ChildArrowScope.drawArrowLine(isLtr: Boolean) { + if (isLtr) { + drawLine( + color = strokeColor, + start = curvedArrowRect.topLeft, + end = Offset(curvedArrowRect.left, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } else { + drawLine( + color = strokeColor, + start = curvedArrowRect.topRight, + end = Offset(curvedArrowRect.right, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 4f8edf75c4..035be68d2e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -29,8 +29,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni modifier = modifier .heightIn(min = TangemTheme.dimens.size52) .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing8, ), icon = { RowIcon( @@ -125,7 +126,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid BlockchainRow( model = state, action = { - TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true) + TangemSwitch(onCheckedChange = { }, checked = true) }, ) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt similarity index 68% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 1917d4e4c8..027db6c7d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component +package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -16,15 +16,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.internal.* +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.token.* -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +import java.util.UUID import kotlin.math.max private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 @@ -34,12 +37,43 @@ private enum class LayoutId { ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT } +/** + * Token item for non reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param modifier modifier + * + * @see Figma Component + */ @Composable -internal fun TokenItem( +fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + TokenItem( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + reorderableTokenListState = null, + ) +} + +/** + * Token item for reorderable list + * + * @param state token item state + * @param isBalanceHidden flag that shows/hides balance + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + * + * @see Figma Component + */ +@Composable +fun TokenItem( state: TokenItemState, isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier, - reorderableTokenListState: ReorderableLazyListState? = null, ) { val betweenRowsMargin = TangemTheme.dimens.spacing2 @@ -73,7 +107,7 @@ internal fun TokenItem( ) TokenPrice( - state = state.cryptoPriceState, + state = state.subtitleState, modifier = Modifier .layoutId(layoutId = LayoutId.CRYPTO_PRICE) .padding(end = TangemTheme.dimens.spacing8), @@ -192,6 +226,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier is TokenItemState.Unreachable, -> { firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + + if (state.subtitleState != null) { + secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width + } } } @@ -229,7 +267,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, - -> (layoutHeight - title.height).div(other = 2) + -> { + if (state.subtitleState == null) { + (layoutHeight - title.height).div(other = 2) + } else { + verticalPadding + } + } else -> verticalPadding }, ) @@ -397,8 +441,8 @@ private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider:: private class TokenItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy( - iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true), + tokenItemVisibleState.copy( + iconState = coinIconState.copy(showCustomBadge = true), titleState = TokenItemState.TitleState.Content( text = "PolygonPolygonPolygonPolygonPolygonPolygon", hasPending = true, @@ -408,19 +452,122 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { + is TokenPriceState.CryptoPriceContent -> { PriceBlock( modifier = modifier, price = state.price, @@ -32,13 +37,12 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi priceChangePercent = state.priceChangePercent, ) } - is TokenPriceChangeState.Unknown -> { - PriceText(text = DASH_SIGN, modifier = modifier) - } - is TokenPriceChangeState.Loading -> { + is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier) + is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier) + is TokenPriceState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) } - is TokenPriceChangeState.Locked -> { + is TokenPriceState.Locked -> { LockedRectangle(modifier = modifier.placeholderSize()) } null -> Unit @@ -122,4 +126,37 @@ private fun Modifier.placeholderSize(): Modifier = composed { return@composed this .padding(vertical = TangemTheme.dimens.spacing2) .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(TokenPriceChangeStateProvider::class) state: TokenPriceState) { + TangemThemePreview { + TokenPrice(state = state) + } +} + +private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.UP, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.DOWN, + ), + TokenPriceState.CryptoPriceContent( + price = "1.234", + priceChangePercent = "2.5%", + type = PriceChangeType.NEUTRAL, + ), + TokenPriceState.TextContent(value = "Subtitle"), + TokenPriceState.Unknown, + TokenPriceState.Loading, + TokenPriceState.Locked, + ), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt similarity index 92% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt index e5b3050a53..706120f20e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component.token +package com.tangem.core.ui.components.token.internal import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image @@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState +import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState @Composable internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt similarity index 50% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 2ece6053fc..9c014e8067 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -1,61 +1,81 @@ -package com.tangem.feature.wallet.presentation.common.state +package com.tangem.core.ui.components.token.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType -/** Token item state */ +/** TokenItem component state */ @Immutable -internal sealed class TokenItemState { +sealed class TokenItemState { + /** Unique id */ abstract val id: String + /** Token icon state */ abstract val iconState: CurrencyIconState + /** Token title state (in one row with [fiatAmountState]) */ abstract val titleState: TitleState + /** Token subtitle state (under [titleState] and in one row with [cryptoAmountState]) */ + abstract val subtitleState: SubtitleState? + + /** Token fiat amount state (in one row with [titleState]) */ abstract val fiatAmountState: FiatAmountState? + /** Token crypto amount state (under [fiatAmountState] and in one row with [subtitleState]) */ abstract val cryptoAmountState: CryptoAmountState? - abstract val cryptoPriceState: CryptoPriceState? - - /** Loading token state */ + /** + * Loading token state + * + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + */ data class Loading( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState.Content, + override val subtitleState: SubtitleState = SubtitleState.Loading, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading } - /** Locked token state */ + /** + * Locked token state + * + * @property id unique id + */ data class Locked(override val id: String) : TokenItemState() { override val iconState: CurrencyIconState = CurrencyIconState.Locked override val titleState: TitleState = TitleState.Locked + override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked - override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked } /** * Content token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property fiatAmountState token fiat amount + * @property cryptoAmountState token crypto amount + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, - override val cryptoPriceState: CryptoPriceState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() @@ -63,9 +83,10 @@ internal sealed class TokenItemState { /** * Draggable token state * - * @property id unique id - * @property iconState token icon state - * @property titleState token name + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property cryptoAmountState token crypto amount */ data class Draggable( override val id: String, @@ -73,48 +94,50 @@ internal sealed class TokenItemState { override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { + override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * Unreachable token state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemClick callback which will be called when an item is clicked - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Unreachable( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } /** * No derivation address state * - * @property id token id - * @property iconState token icon state - * @property titleState token name - * @property onItemLongClick callback which will be called when an item is long clicked + * @property id unique id + * @property iconState token icon state + * @property titleState token title + * @property subtitleState token subtitle + * @property onItemLongClick callback which will be called when an item is long clicked */ data class NoAddress( override val id: String, override val iconState: CurrencyIconState, override val titleState: TitleState, + override val subtitleState: SubtitleState? = null, val onItemLongClick: () -> Unit, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val cryptoPriceState: CryptoPriceState? = null } @Immutable @@ -122,9 +145,27 @@ internal sealed class TokenItemState { data class Content(val text: String, val hasPending: Boolean = false) : TitleState() - object Loading : TitleState() + data object Loading : TitleState() - object Locked : TitleState() + data object Locked : TitleState() + } + + @Immutable + sealed class SubtitleState { + + data class CryptoPriceContent( + val price: String, + val priceChangePercent: String, + val type: PriceChangeType, + ) : SubtitleState() + + data class TextContent(val value: String) : SubtitleState() + + data object Unknown : SubtitleState() + + data object Loading : SubtitleState() + + data object Locked : SubtitleState() } @Immutable @@ -134,34 +175,19 @@ internal sealed class TokenItemState { val hasStaked: Boolean = false, ) : FiatAmountState() - object Loading : FiatAmountState() + data object Loading : FiatAmountState() - object Locked : FiatAmountState() + data object Locked : FiatAmountState() } @Immutable sealed class CryptoAmountState { data class Content(val text: String) : CryptoAmountState() - object Unreachable : CryptoAmountState() + data object Unreachable : CryptoAmountState() - object Loading : CryptoAmountState() + data object Loading : CryptoAmountState() - object Locked : CryptoAmountState() - } - - sealed class CryptoPriceState { - - data class Content( - val price: String, - val priceChangePercent: String?, - val type: PriceChangeType?, - ) : CryptoPriceState() - - object Unknown : CryptoPriceState() - - object Loading : CryptoPriceState() - - object Locked : CryptoPriceState() + data object Locked : CryptoAmountState() } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableBottomSheetComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableBottomSheetComponent.kt new file mode 100644 index 0000000000..2e3d83710f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableBottomSheetComponent.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.decompose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable + +@Stable +interface ComposableBottomSheetComponent { + + fun dismiss() + + @Composable + fun BottomSheet() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 0347ca4893..dd15fd2212 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable @@ -14,12 +16,14 @@ fun TangemThemePreview( typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, alwaysShowBottomSheets: Boolean = true, + rtl: Boolean = false, content: @Composable () -> Unit, ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() CompositionLocalProvider( LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets, + LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr, ) { BoxWithConstraints { TangemTheme( diff --git a/core/ui/src/main/res/drawable/ic_tether_24.xml b/core/ui/src/main/res/drawable/ic_tether_24.xml new file mode 100644 index 0000000000..3c53fbab8e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tether_24.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tether_28.xml b/core/ui/src/main/res/drawable/ic_tether_28.xml deleted file mode 100644 index e3f94b5dbb..0000000000 --- a/core/ui/src/main/res/drawable/ic_tether_28.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml new file mode 100644 index 0000000000..977d693e60 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index cb2081979e..c1855597a2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -1,6 +1,7 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -20,10 +21,7 @@ internal object CardInfoConverter : Converter { CardInfo( userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, - cardsCount = when (val status = value.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.toString() - else -> "0" - }, + cardsCount = value.getBackupCardsCount()?.toString() ?: "0", firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index de9edf3b4b..fa6e9a355e 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -108,6 +108,22 @@ internal class DefaultMarketsTokenRepository( return TokenChartConverter.convert(interval, response.getOrThrow()) } + override suspend fun getChartPreview( + fiatCurrencyCode: String, + interval: PriceChangeInterval, + tokenId: String, + ): TokenChart { + val response = marketsApi.getCoinsListCharts( + coinIds = tokenId, + currency = fiatCurrencyCode, + interval = interval.toRequestParam(), + ) + + val chart = response.getOrThrow()[tokenId] ?: error("No chart preview data for token $tokenId") + + return TokenChartConverter.convert(interval, chart) + } + override suspend fun getTokenInfo( fiatCurrencyCode: String, tokenId: String, diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 9ce502abe3..343c8f3116 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,10 +2,13 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey @@ -22,6 +25,7 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync @@ -45,7 +49,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -67,6 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, + moshi: Moshi, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -104,6 +108,13 @@ internal class DefaultStakingRepository( value = emptyMap(), ) + private val tronStakeKitTransactionAdapter: JsonAdapter = + moshi.adapter(TronStakeKitTransaction::class.java) + + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { + rawNetworkId.plus(rawCurrencyId) + } + override fun isStakingSupported(integrationKey: String): Boolean { return integrationIdMap.containsKey(integrationKey) } @@ -141,8 +152,8 @@ internal class DefaultStakingRepository( val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), - periodInDays = yield.metadata.cooldownPeriod.days, + apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), + rewardSchedule = yield.metadata.rewardSchedule, tokenSymbol = yield.token.symbol, ) } @@ -158,7 +169,7 @@ internal class DefaultStakingRepository( val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) - val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey()) + val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrencyId)) when { prefetchedYield != null && isSupported -> { @@ -261,25 +272,27 @@ internal class DefaultStakingRepository( override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - val cryptoCurrency = address.cryptoCurrency - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: return@withContext + + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val requestBody = getBalanceRequestData(address.address, integrationId) + val requestBody = getBalanceRequestData(address, integrationId) val result = stakeKitApi.getSingleYieldBalance( integrationId = requestBody.integrationId, body = requestBody, ).getOrThrow() stakingBalanceStore.store( + userWalletId, requestBody.integrationId, YieldBalanceWrapperDTO( balances = result, @@ -292,15 +305,15 @@ internal class DefaultStakingRepository( override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - stakingBalanceStore.get(integrationId) + stakingBalanceStore.get(userWalletId, integrationId) .collectLatest { send( yieldBalanceConverter.convert( @@ -316,7 +329,7 @@ internal class DefaultStakingRepository( withContext(dispatchers.io) { fetchSingleYieldBalance( userWalletId, - address, + cryptoCurrency, ) } } @@ -324,16 +337,18 @@ internal class DefaultStakingRepository( override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalance.Empty } else { - fetchSingleYieldBalance(userWalletId, address) + fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error + val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId) + ?: return@withContext YieldBalance.Error + yieldBalanceConverter.convert( YieldBalanceConverter.Data( balance = result, @@ -345,7 +360,7 @@ internal class DefaultStakingRepository( override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext @@ -357,23 +372,23 @@ internal class DefaultStakingRepository( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val result = stakeKitApi.getMultipleYieldBalances( - addresses - .mapNotNull { networkAddress -> - val cryptoCurrency = networkAddress.cryptoCurrency - val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] + val availableCurrencies = cryptoCurrencies + .mapNotNull { currency -> + val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network) + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - if (integrationId != null) { - networkAddress.address to integrationId - } else { - null - } + if (integrationId != null && address != null) { + address to integrationId + } else { + null } - .distinct() - .map { getBalanceRequestData(it.first, it.second) }, - ).getOrThrow() + } + .distinct() + .map { getBalanceRequestData(it.first, it.second) } + .ifEmpty { return@invokeOnExpire } + val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() - stakingBalanceStore.store(result) + stakingBalanceStore.store(userWalletId, result) }, ) } finally { @@ -385,20 +400,20 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { - stakingBalanceStore.get() + stakingBalanceStore.get(userWalletId) .collectLatest { send(yieldBalanceListConverter.convert(it)) } } withContext(dispatchers.io) { fetchMultiYieldBalance( userWalletId, - addresses, + cryptoCurrencies, ) } } @@ -406,14 +421,14 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { launch(dispatchers.io) { combine( - stakingBalanceStore.get(), + stakingBalanceStore.get(userWalletId), isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, ) { result, isFetching -> val balances = yieldBalanceListConverter.convert(result) @@ -422,7 +437,7 @@ internal class DefaultStakingRepository( } withContext(dispatchers.io) { catch( - block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) }, + block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) }, catch = { raise(it) }, ) } @@ -431,13 +446,13 @@ internal class DefaultStakingRepository( override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) { YieldBalanceList.Empty } else { - fetchMultiYieldBalance(userWalletId, addresses) - val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error + fetchMultiYieldBalance(userWalletId, cryptoCurrencies) + val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error yieldBalanceListConverter.convert(result) } } @@ -506,6 +521,8 @@ internal class DefaultStakingRepository( amount = params.amount.toPlainString(), inputToken = tokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, + validatorAddresses = listOf(params.validatorAddress), // check on other networks + tronResource = getTronResource(network), ), ) } @@ -539,16 +556,8 @@ internal class DefaultStakingRepository( } } - override fun isStakeMoreAvailable(networkId: Network.ID): Boolean { - val blockchain = Blockchain.fromId(networkId.value) - return when (blockchain) { - Blockchain.Solana -> false - else -> true - } - } - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - return when (cryptoCurrency.id.getIntegrationKey()) { + return when (getIntegrationKey(cryptoCurrency.id)) { Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> { StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) } @@ -562,8 +571,14 @@ internal class DefaultStakingRepository( Blockchain.Solana, Blockchain.Cosmos, -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) + Blockchain.BSC, Blockchain.Ethereum, -> TransactionData.Compiled.Data.RawString(unsignedTransaction) + Blockchain.Tron -> { + val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction) + ?: error("Failed to parse Tron StakeKit transaction") + TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex) + } else -> error("Unsupported blockchain") } } @@ -593,7 +608,15 @@ internal class DefaultStakingRepository( private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" - private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) + private fun getTronResource(network: Network): TronResource? { + val blockchain = Blockchain.fromNetworkId(network.backendId) + + return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) { + TronResource.ENERGY + } else { + null + } + } private companion object { const val YIELDS_STORE_KEY = "yields" @@ -601,11 +624,11 @@ internal class DefaultStakingRepository( const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" + const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" const val TRON_INTEGRATION_ID = "tron-trx-native-staking" const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" @@ -617,11 +640,11 @@ internal class DefaultStakingRepository( Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, - // Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, + Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, - // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, // Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index b1c9d7d47c..ec86b52e73 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -26,6 +26,7 @@ internal class YieldBalanceConverter : Converter, YieldBalanceList> { +internal class YieldBalanceListConverter : Converter, YieldBalanceList> { internal val converter by lazy(LazyThreadSafetyMode.NONE) { YieldBalanceConverter() } - override fun convert(value: List): YieldBalanceList { + override fun convert(value: Set): YieldBalanceList { return if (value.isEmpty()) { YieldBalanceList.Empty } else { diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index d261cdece4..32a9a1cdae 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -37,6 +37,7 @@ internal object StakingDataModule { stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, + @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, @@ -47,6 +48,7 @@ internal object StakingDataModule { cacheRegistry = cacheRegistry, stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, + moshi = moshi, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 59ddeeb01d..c37a3bc87f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,7 +27,7 @@ internal object TokensDataModule { fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, expressAssetsStore: ExpressAssetsStore, @@ -38,7 +37,7 @@ internal object TokensDataModule { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, expressAssetsStore = expressAssetsStore, @@ -71,7 +70,7 @@ internal object TokensDataModule { networksStatusesStore: NetworksStatusesStore, walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, - userTokensStore: UserTokensStore, + appPreferencesStore: AppPreferencesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { @@ -79,7 +78,7 @@ internal object TokensDataModule { networksStatusesStore = networksStatusesStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - userTokensStore = userTokensStore, + appPreferencesStore = appPreferencesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ec3720fe1b..5e4c6b350e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -18,8 +18,12 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation @@ -46,11 +50,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, - private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -86,7 +90,7 @@ internal class DefaultCurrenciesRepository( override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, ) @@ -141,7 +145,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, ) @@ -163,7 +167,7 @@ internal class DefaultCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { return withContext(dispatchers.io) { val savedCurrencies = requireNotNull( - value = userTokensStore.getSyncOrNull(userWalletId), + value = getSavedUserTokensResponseSync(key = userWalletId), lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, ) @@ -276,13 +280,31 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet, refresh) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWallet.walletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } + override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) = + withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) + + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWallet.walletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) + + responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) + } + override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, @@ -290,9 +312,12 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrency( currencyId = id, @@ -312,9 +337,12 @@ internal class DefaultCurrenciesRepository( fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) val blockchain = Blockchain.fromId(networkId.value) val blockchainNetworkId = blockchain.toNetworkId() val coinId = blockchain.toCoinId() @@ -335,7 +363,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.group == UserTokensResponse.GroupType.NETWORK } .collect(::send) } @@ -347,7 +375,7 @@ internal class DefaultCurrenciesRepository( ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - userTokensStore.get(userWalletId) + getSavedUserTokensResponse(userWalletId) .map { it.sort == UserTokensResponse.SortType.BALANCE } .collect(::send) } @@ -461,9 +489,14 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) fetchTokensIfCacheExpired(userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val storedTokens = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) return storedTokens.tokens.any { it.contractAddress != null && @@ -473,7 +506,7 @@ internal class DefaultCurrenciesRepository( } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { - return userTokensStore.get(userWallet.walletId).map { storedTokens -> + return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( response = storedTokens, scanResponse = userWallet.scanResponse, @@ -517,17 +550,26 @@ internal class DefaultCurrenciesRepository( .let { customTokensMerger.mergeIfPresented(userWalletId, response) } .let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated) - userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue), + value = compatibleUserTokensResponse, + ) + fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse) } private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean { - return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null + val response = getSavedUserTokensResponseSync(key = userWallet.walletId) + + return demoConfig.isDemoCardId(userWallet.cardId) && response == null } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) - userTokensStore.store(userWalletId, compatibleUserTokensResponse) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) pushTokens(userWalletId, response) } @@ -561,8 +603,9 @@ internal class DefaultCurrenciesRepository( private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId - val response = userTokensStore.getSyncOrNull(userWalletId) - ?: createDefaultUserTokensResponse(userWallet) + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) ?: createDefaultUserTokensResponse(userWallet) if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") @@ -622,4 +665,16 @@ internal class DefaultCurrenciesRepository( } private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private fun getSavedUserTokensResponse(key: UserWalletId): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) + .filterNotNull() + } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index de6ad111ab..3762b4ddee 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -8,8 +8,11 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.LceFlow @@ -33,7 +36,7 @@ internal class DefaultNetworksRepository( private val networksStatusesStore: NetworksStatusesStore, private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, - private val userTokensStore: UserTokensStore, + private val appPreferencesStore: AppPreferencesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : NetworksRepository { @@ -300,9 +303,14 @@ internal class DefaultNetworksRepository( } return if (userWallet.isMultiCurrency) { - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } + val response = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index a3cbd7d4f8..30d5dd83a8 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -86,6 +86,12 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") } + fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) { + builder.appendKeyValue("Validator", validatorName ?: "unknown") + builder.appendKeyValue("Action", transactionType ?: "unknown") + builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt index 596699a073..687a9859d1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -14,7 +14,7 @@ class SaveBlockchainErrorUseCase( private val feedbackRepository: FeedbackRepository, ) { - fun invoke(error: BlockchainErrorInfo) { + operator fun invoke(error: BlockchainErrorInfo) { feedbackRepository.saveBlockchainErrorInfo(error = error) } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1e4971e4cf..1be0afb1ff 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -22,4 +22,12 @@ sealed interface FeedbackEmailType { /** User has problem with sending transaction */ data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType + + /** User has problem with staking */ + data class StakingProblem( + override val cardInfo: CardInfo, + val validatorName: String?, + val transactionType: String?, + val unsignedTransaction: String?, + ) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index a9bb1a64dd..c314b7a4bc 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -23,6 +23,12 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) + is FeedbackEmailType.StakingProblem -> addStakingProblemBody( + type.cardInfo, + type.validatorName, + type.transactionType, + type.unsignedTransaction, + ) } return build() @@ -72,6 +78,36 @@ internal class EmailMessageBodyResolver( addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } + private suspend fun FeedbackDataBuilder.addStakingProblemBody( + cardInfo: CardInfo, + validatorName: String?, + transactionType: String?, + unsignedTransaction: String?, + ) { + addCardInfo(cardInfo) + addDelimiter() + + val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addStakingInfo(validatorName, transactionType, unsignedTransaction) + addDelimiter() + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { addCardInfo(cardInfo) addDelimiter() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 6ab001b775..bfa9cfdede 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -19,7 +19,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed - is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + is FeedbackEmailType.TransactionSendingProblem, + is FeedbackEmailType.StakingProblem, + -> R.string.feedback_preface_tx_failed } .let(resources::getString) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index beb89739bd..503f1f514b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -26,6 +26,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.StakingProblem -> R.string.feedback_subject_tx_failed } .let(resources::getString) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 55cc073069..29ce08c107 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -10,54 +10,8 @@ interface CardTypesResolver { fun isTangemWallet(): Boolean - fun isWhiteWallet2(): Boolean - - fun isAvroraWallet(): Boolean - - fun isTraillantWallet(): Boolean - fun isShibaWallet(): Boolean - fun isTronWallet(): Boolean - - fun isKaspaWallet(): Boolean - - fun isKaspa2Wallet(): Boolean - - fun isKaspaResellerWallet(): Boolean - - fun isBadWallet(): Boolean - - fun isJrWallet(): Boolean - - fun isGrimWallet(): Boolean - - fun isSatoshiFriendsWallet(): Boolean - - fun isBitcoinPizzaDayWallet(): Boolean - - fun isVeChainWallet(): Boolean - - fun isNewWorldEliteWallet(): Boolean - - fun isRedPandaWallet(): Boolean - - fun isCryptoSethWallet(): Boolean - - fun isKishuInuWallet(): Boolean - - fun isBabyDogeWallet(): Boolean - - fun isCOQWallet(): Boolean - - fun isCoinMetricaWallet(): Boolean - - fun isVoltInuWallet(): Boolean - - fun isVividWallet(): Boolean - - fun isPastelWallet(): Boolean - fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 6a4afdcfd9..85f6165a23 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -26,60 +26,10 @@ internal class TangemCardTypesResolver( card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable } - override fun isWhiteWallet2(): Boolean = card.batchId == WHITE_WALLET2_BATCH_ID - - override fun isAvroraWallet(): Boolean = card.batchId == AVRORA_WALLET_BATCH_ID - - override fun isTraillantWallet(): Boolean = card.batchId == TRILLIANT_WALLET_BATCH_ID - override fun isShibaWallet(): Boolean { return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0 } - override fun isTronWallet(): Boolean = card.batchId == TRON_WALLET_BATCH_ID - - override fun isKaspaWallet(): Boolean = card.batchId == KASPA_WALLET_BATCH_ID - - override fun isKaspa2Wallet(): Boolean = card.batchId == KASPA2_WALLET_BATCH_ID - - override fun isKaspaResellerWallet(): Boolean = card.batchId == KASPA_RESELLER_WALLET_BATCH_ID - - override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID - - override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID - - override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID - - override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID - - override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID - - override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID - - override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID - - override fun isRedPandaWallet(): Boolean = card.batchId == RED_PANDA_WALLET_BATCH_ID - - override fun isCryptoSethWallet(): Boolean = card.batchId == CRYPTO_SETH_WALLET_BATCH_ID - - override fun isKishuInuWallet(): Boolean = card.batchId == KISHU_INU_WALLET_BATCH_ID - - override fun isBabyDogeWallet(): Boolean = card.batchId == BABY_DOGE_WALLET_BATCH_ID - - override fun isCOQWallet(): Boolean = card.batchId == COQ_WALLET_BATCH_ID - - override fun isCoinMetricaWallet(): Boolean = card.batchId == COIN_METRICA_WALLET_BATCH_ID - - override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID - - override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID || - card.batchId == VIVID_AQUA_WALLET_BATCH_ID || - card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID - - override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID || - card.batchId == PASTEL_GRASS_WALLET_BATCH_ID || - card.batchId == PASTEL_AIR_WALLET_BATCH_ID - override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -183,34 +133,5 @@ internal class TangemCardTypesResolver( private companion object { const val DEV_KIT_CARD_BATCH_ID = "CB83" - const val TRON_WALLET_BATCH_ID = "AF07" - const val KASPA_WALLET_BATCH_ID = "AF08" - const val KASPA2_WALLET_BATCH_ID = "AF25" - const val KASPA_RESELLER_WALLET_BATCH_ID = "AF31" - const val BAD_WALLET_BATCH_ID = "AF09" - const val JR_WALLET_BATCH_ID = "AF14" - const val GRIM_WALLET_BATCH_ID = "AF13" - const val SATOSHI_WALLET_BATCH_ID = "AF19" - const val WHITE_WALLET2_BATCH_ID = "AF15" - const val TRILLIANT_WALLET_BATCH_ID = "AF16" - const val AVRORA_WALLET_BATCH_ID = "AF18" - const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" - const val VECHAIN_WALLET_BATCH_ID = "AF29" - const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" - const val RED_PANDA_WALLET_BATCH_ID = "AF34" - const val CRYPTO_SETH_WALLET_BATCH_ID = "AF32" - const val KISHU_INU_WALLET_BATCH_ID = "AF52" - const val BABY_DOGE_WALLET_BATCH_ID = "AF51" - const val COQ_WALLET_BATCH_ID = "AF28" - const val COIN_METRICA_WALLET_BATCH_ID = "AF27" - const val VOLT_INU_WALLET_BATCH_ID = "AF35" - // VIVID WALLETS - const val VIVID_LEMON_WALLET_BATCH_ID = "AF40" - const val VIVID_AQUA_WALLET_BATCH_ID = "AF41" - const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42" - // PASTEL WALLETS - const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43" - const val PASTEL_AIR_WALLET_BATCH_ID = "AF44" - const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45" } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt similarity index 73% rename from domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt rename to domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 1c3519f606..94727f02cb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -12,6 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.Wallet2CardConfig +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet @@ -32,8 +33,6 @@ val UserWallet.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null -fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed -fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean { return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) @@ -66,4 +65,37 @@ private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: Der val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false val extendedPublicKey = extendedPublicKeysMap[derivationPath] return extendedPublicKey != null +} + +/** + * Get total cards count in wallets set for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getCardsCount(): Int? { + if (!cardTypesResolver.isMultiwalletAllowed()) return null + + return when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + 1 + is CardDTO.BackupStatus.NoBackup, + is CardDTO.BackupStatus.CardLinked, + null, // Multi-currency wallet without backup function. Example, 4.12 + -> 1 + } +} + +/** + * Get backup cards count for this [ScanResponse] card + * + * @return null if wallet is not multi-currency or total cards count + */ +fun ScanResponse.getBackupCardsCount(): Int? { + return if (cardTypesResolver.isMultiwalletAllowed()) { + when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + else -> 0 + } + } else { + null + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt new file mode 100644 index 0000000000..0b5ef6f2fd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletExt.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.common.util + +import com.tangem.domain.wallets.models.UserWallet + +/** + * Get total cards count in wallets set for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getCardsCount(): Int? = scanResponse.getCardsCount() + +/** + * Get backup cards count for a card that was saved in [UserWallet] + * + * @return null if wallet is not multi-currency or total cards count + */ +fun UserWallet.getBackupCardsCount(): Int? = scanResponse.getBackupCardsCount() \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 68f4060640..7b187af646 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -376,15 +376,12 @@ class DefaultWalletManagersFacade( return walletManagersStore.getAllSync(userWalletId) } - @Deprecated( - "Use NetworkAddress from CryptoCurrencyStatus", - ReplaceWith("cryptoCurrencyStatus.value.networkAddress"), - ) - override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ - return getAddresses(userWalletId, network).sortedBy { it.type } + override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? { + return getAddresses(userWalletId, network) + .firstOrNull { it.type == AddressType.Default } + ?.value } - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ val manager = getOrCreateWalletManager( userWalletId = userWalletId, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4ecfbe3805..e55a359854 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -117,20 +117,18 @@ interface WalletManagersFacade { suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List /** - * Returns ordered list of addresses for selected wallet for given currency + * Returns default network address for selected wallet in given network * * @param userWalletId selected wallet id * @param network network of currency */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") - suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? /** Returns list of all addresses for all currencies in selected wallet * * @param userWalletId selected wallet id * @param network required to create wallet manager */ - @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
/** diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 885998c263..c0461ee9ab 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -11,12 +11,15 @@ android { dependencies { + /* Domain */ api(projects.domain.appCurrency.models) api(projects.domain.core) api(projects.core.pagination) api(projects.domain.markets.models) - - implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + + /* Utils */ + implementation(deps.kotlin.serialization) implementation(projects.core.utils) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt similarity index 60% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt rename to domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt index a1aa6b178c..643846c466 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt @@ -1,35 +1,32 @@ -package com.tangem.features.markets.details.api +package com.tangem.domain.markets import com.tangem.domain.core.serialization.SerializedBigDecimal -import com.tangem.domain.markets.TokenMarket import kotlinx.serialization.Serializable @Serializable -data class TokenMarketSerializable( +data class TokenMarketParams( val id: String, val name: String, val symbol: String, - val marketCap: SerializedBigDecimal?, val tokenQuotes: Quotes, - val imageUrl: String, + val imageUrl: String?, ) { @Serializable data class Quotes( val currentPrice: SerializedBigDecimal, val h24Percent: SerializedBigDecimal, - val weekPercent: SerializedBigDecimal, - val monthPercent: SerializedBigDecimal, + val weekPercent: SerializedBigDecimal?, + val monthPercent: SerializedBigDecimal?, ) } -fun TokenMarket.toSerializable(): TokenMarketSerializable { - return TokenMarketSerializable( +fun TokenMarket.toSerializableParam(): TokenMarketParams { + return TokenMarketParams( id = id, name = name, symbol = symbol, - marketCap = marketCap, - tokenQuotes = TokenMarketSerializable.Quotes( + tokenQuotes = TokenMarketParams.Quotes( currentPrice = tokenQuotesShort.currentPrice, h24Percent = tokenQuotesShort.h24ChangePercent, weekPercent = tokenQuotesShort.weekChangePercent, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt new file mode 100644 index 0000000000..a755057a98 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTokenFullQuotesUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { + return Either.catch { + marketsTokenRepository.getTokenQuotes( + fiatCurrencyCode = appCurrency.code, + tokenId = tokenId, + ) + }.mapLeft {} + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt index c686ff3874..feeeadab56 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt @@ -12,13 +12,22 @@ class GetTokenPriceChartUseCase( appCurrency: AppCurrency, interval: PriceChangeInterval, tokenId: String, + preview: Boolean, ): Either { return Either.catch { - marketsTokenRepository.getChart( - fiatCurrencyCode = appCurrency.code, - interval = interval, - tokenId = tokenId, - ) + if (preview) { + marketsTokenRepository.getChartPreview( + fiatCurrencyCode = appCurrency.code, + interval = interval, + tokenId = tokenId, + ) + } else { + marketsTokenRepository.getChart( + fiatCurrencyCode = appCurrency.code, + interval = interval, + tokenId = tokenId, + ) + } }.mapLeft {} } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt index 6477010d57..059c80ac82 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt @@ -1,19 +1,26 @@ package com.tangem.domain.markets import arrow.core.Either -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +@Suppress("UnusedPrivateMember") class GetTokenQuotesUseCase( - private val marketsTokenRepository: MarketsTokenRepository, + private val quotesRepository: QuotesRepository, ) { - - suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { - return Either.catch { - marketsTokenRepository.getTokenQuotes( - fiatCurrencyCode = appCurrency.code, - tokenId = tokenId, - ) - }.mapLeft {} + operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow> { + return flowOf( + Either.catch { + Quote( + rawCurrencyId = "USD", + fiatRate = 100.toBigDecimal(), // mock + priceChange = 10.toBigDecimal(), // mock + ) + }.mapLeft {}, + ) + // TODO implement quotes fetching from repository [REDACTED_TASK_KEY] + // quotesRepository.getQuotesUpdates() } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 5c80144aec..e96aa4525c 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -12,6 +12,8 @@ interface MarketsTokenRepository { suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + suspend fun getChartPreview(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt index 691aece740..c400337e61 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -1,9 +1,10 @@ package com.tangem.domain.staking.model +import com.tangem.domain.staking.model.stakekit.Yield import java.math.BigDecimal data class StakingEntryInfo( - val interestRate: BigDecimal, - val periodInDays: Int, + val apr: BigDecimal, + val rewardSchedule: Yield.Metadata.RewardSchedule, val tokenSymbol: String, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 35072a4cb1..adf85b9751 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -1,6 +1,7 @@ package com.tangem.domain.staking.model.stakekit import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import org.joda.time.DateTime import java.math.BigDecimal sealed class YieldBalance { @@ -38,6 +39,7 @@ data class BalanceItem( val rawCurrencyId: String?, val rawNetworkId: String, val validatorAddress: String?, + val date: DateTime?, val pendingActions: List, ) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index 01cc23bdc9..042aa9acbf 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -5,13 +5,10 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?, networkName: String): YieldBalance { + fun getBalance(rawCurrencyId: String?): YieldBalance { return balances.firstOrNull { yield -> (yield as? YieldBalance.Data)?.balance?.items - ?.any { - rawCurrencyId == it.rawCurrencyId && - networkName.equals(it.rawNetworkId, ignoreCase = true) - } == true + ?.any { rawCurrencyId == it.rawCurrencyId } == true } ?: YieldBalance.Error } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 333762d0e1..e09a9309c7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.raise.either import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( @@ -16,7 +16,7 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ): Either { return either { @@ -24,7 +24,7 @@ class FetchStakingYieldBalanceUseCase( block = { stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, refresh = refresh, ) }, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 5c5528b931..16d17d97ad 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -20,11 +20,11 @@ class GetStakingYieldBalanceUseCase( operator fun invoke( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, - address = address, + cryptoCurrency = cryptoCurrency, ).map> { it.right() } .catch { emit(stakingErrorResolver.resolve(it).left()) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt deleted file mode 100644 index 4dd7fc8f38..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.Network - -class IsStakeMoreAvailableUseCase( - private val stakingRepository: StakingRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke(networkId: Network.ID): Either { - return Either - .catch { stakingRepository.isStakeMoreAvailable(networkId) } - .mapLeft { stakingErrorResolver.resolve(it) } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5b6604423e..69016e1209 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -15,7 +15,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -23,6 +22,8 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface StakingRepository { + fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String + fun isStakingSupported(integrationKey: String): Boolean suspend fun fetchEnabledYields(refresh: Boolean) @@ -38,33 +39,33 @@ interface StakingRepository { suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean = false, ) - fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow + fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean = false, ) fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction @@ -83,9 +84,6 @@ interface StakingRepository { suspend fun sendUnsubmittedHashes() - /** Returns whether additional staking is possible if there is already active staking */ - fun isStakeMoreAvailable(networkId: Network.ID): Boolean - /** Returns staking approval */ fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index ca9266b2d1..e40ecd7e98 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -44,6 +44,7 @@ class FetchCardTokenListUseCase( val yieldBalances = async { fetchYieldBalances( userWalletId = userWalletId, + currencies = currencies, refresh = refresh, ) } @@ -77,10 +78,13 @@ class FetchCardTokenListUseCase( ) } - private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + private suspend fun fetchYieldBalances( + userWalletId: UserWalletId, + currencies: List, + refresh: Boolean, + ) { catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) }, + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, catch = { /* Ignore error */ }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 8fbd29d4f5..7b631669e7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -29,6 +30,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { /** @@ -80,8 +82,11 @@ class FetchCurrencyStatusUseCase( val fetchQuote = async { fetchQuote(currency.id, refresh) } + val fetchStakingBalance = async { + fetchStakingBalance(userWalletId, currency, refresh) + } - awaitAll(fetchStatus, fetchQuote) + awaitAll(fetchStatus, fetchQuote, fetchStakingBalance) } private suspend fun Raise.getCurrency( @@ -122,4 +127,16 @@ class FetchCurrencyStatusUseCase( raise(CurrencyStatusError.DataError(it)) } } + + private suspend fun Raise.fetchStakingBalance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + refresh: Boolean, + ) { + catch( + block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt index e108b21819..aeba9d2c14 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -20,7 +20,7 @@ class RefreshMultiCurrencyWalletQuotesUseCase( suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { - val currencies = fetchCurrencies(userWalletId = userWalletId) + val currencies = getCurrencies(userWalletId = userWalletId) .getOrElse { raise(QuotesError.DataError(it)) } coroutineScope { @@ -35,10 +35,10 @@ class RefreshMultiCurrencyWalletQuotesUseCase( } } - private suspend fun fetchCurrencies(userWalletId: UserWalletId): Either> { + private suspend fun getCurrencies(userWalletId: UserWalletId): Either> { return either { catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, false) }, + block = { currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId) }, catch = { raise(it) }, ) } @@ -46,7 +46,9 @@ class RefreshMultiCurrencyWalletQuotesUseCase( private suspend fun fetchQuotes(currenciesIds: Set) { catch( - block = { quotesRepository.fetchQuotes(currenciesIds) }, + block = { + quotesRepository.fetchQuotes(currenciesIds) + }, catch = { /* Ignore error */ }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 18f07bea3e..8d97dc1a83 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -69,11 +69,10 @@ internal class CurrenciesStatusesLceOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - val addresses = networksRepository.getNetworkAddresses(userWalletId) combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), - getYieldBalances(userWalletId, addresses), + getYieldBalances(userWalletId, nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> val statuses = createCurrenciesStatuses( currencies = nonEmptyCurrencies, @@ -145,10 +144,14 @@ internal class CurrenciesStatusesLceOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, @@ -196,11 +199,11 @@ internal class CurrenciesStatusesLceOperations( private fun getYieldBalances( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow { return stakingRepository.getMultiYieldBalanceLce( userWalletId = userWalletId, - addresses = addresses, + cryptoCurrencies = cryptoCurrencies, ).map { maybeBalances -> maybeBalances.mapError { TokenListError.DataError(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index fd9870c235..74e207b2e7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -11,7 +11,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* // FIXME: Refactor - [REDACTED_JIRA] @@ -35,7 +34,7 @@ internal class CurrenciesStatusesOperations( val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - val yieldBalances = getYieldBalancesSync() + val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) }, @@ -147,7 +146,7 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - getYieldBalances(), + getYieldBalances(nonEmptyCurrencies), ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) } @@ -261,10 +260,14 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( - rawCurrencyId = currency.id.rawCurrencyId, - networkName = currency.network.name, + val isStakingSupported = stakingRepository.isStakingSupported( + stakingRepository.getIntegrationKey(currency.id), ) + val yieldBalance = if (isStakingSupported) { + (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + } else { + null + } createCurrencyStatus( currency = currency, quote = quote, @@ -385,25 +388,23 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } - @OptIn(ExperimentalCoroutinesApi::class) - private fun getYieldBalances(): EitherFlow { - return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses -> - stakingRepository.getMultiYieldBalanceFlow( - userWalletId = userWalletId, - addresses = addresses, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + private fun getYieldBalances(cryptoCurrencies: List): Flow> { + return stakingRepository.getMultiYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } - private suspend fun getYieldBalancesSync(): Either { + private suspend fun getYieldBalancesSync( + cryptoCurrencies: List, + ): Either { return catch( block = { - val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) stakingRepository.getMultiYieldBalanceSync( userWalletId, - networkAddresses, + cryptoCurrencies, ).right() }, catch = { @@ -417,10 +418,9 @@ internal class CurrenciesStatusesOperations( ): Either { return catch( block = { - val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency) stakingRepository.getSingleYieldBalanceSync( userWalletId, - address, + cryptoCurrency, ).right() }, catch = { @@ -429,19 +429,13 @@ internal class CurrenciesStatusesOperations( ) } - @OptIn(ExperimentalCoroutinesApi::class) private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { - return networksRepository.getNetworkAddressFlow( - userWalletId, - cryptoCurrency, - ).flatMapLatest { address -> - stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - address = address, - ).map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } - } + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } } private fun getIds( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 37efae4b4b..1bd10511b8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -148,6 +148,17 @@ interface CurrenciesRepository { refresh: Boolean = false, ): List + /** + * Retrieves the list of cryptocurrencies within a multi-currency wallet. + * Returns previously loaded currencies or empty list + * + * @param userWalletId The unique identifier of the user wallet. + * @return A list of [CryptoCurrency]. + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId): List + /** * Retrieves the cryptocurrency for a specific multi-currency user wallet. * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 8ef1dfea78..17f718c351 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -65,6 +65,10 @@ internal class MockCurrenciesRepository( return tokens.first().getOrElse { e -> throw e } } + override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId): List { + return tokens.first().getOrElse { e -> throw e } + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return token.getOrElse { e -> throw e } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 33c6654e89..9bc782020a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -16,7 +16,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -25,6 +24,9 @@ import org.joda.time.DateTime import java.math.BigDecimal class MockStakingRepository : StakingRepository { + + override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = "" + override fun isStakingSupported(currencyId: String): Boolean = true override suspend fun fetchEnabledYields(refresh: Boolean) { @@ -33,9 +35,9 @@ class MockStakingRepository : StakingRepository { override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo = StakingEntryInfo( - interestRate = 1.toBigDecimal(), - periodInDays = 2, + apr = 1.toBigDecimal(), tokenSymbol = "SOL", + rewardSchedule = Yield.Metadata.RewardSchedule.DAY, ) override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( @@ -118,7 +120,7 @@ class MockStakingRepository : StakingRepository { override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, refresh: Boolean, ) { /* no-op */ @@ -126,19 +128,19 @@ class MockStakingRepository : StakingRepository { override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { send(YieldBalance.Error) } override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, - address: CryptoCurrencyAddress, + cryptoCurrency: CryptoCurrency, ): YieldBalance = YieldBalance.Error override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, refresh: Boolean, ) { /* no-op */ @@ -146,7 +148,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): Flow = channelFlow { send( YieldBalanceList.Data( @@ -157,7 +159,7 @@ class MockStakingRepository : StakingRepository { override fun getMultiYieldBalanceLce( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): LceFlow = lceFlow { send( YieldBalanceList.Data( @@ -168,7 +170,7 @@ class MockStakingRepository : StakingRepository { override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, - addresses: List, + cryptoCurrencies: List, ): YieldBalanceList = YieldBalanceList.Data( balances = listOf(YieldBalance.Error), ) @@ -249,7 +251,5 @@ class MockStakingRepository : StakingRepository { /* no-op */ } - override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt similarity index 81% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt index 3f5f7485d4..afa825d759 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,7 +1,5 @@ package com.tangem.domain.transaction.error -import com.tangem.core.ui.extensions.TextReference - sealed class SendTransactionError { data object DemoCardError : SendTransactionError() @@ -16,7 +14,7 @@ sealed class SendTransactionError { data class CreateAccountUnderfunded(val amount: String) : SendTransactionError() - data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class TangemSdkError(val code: Int, val messageRes: Int, val args: List) : SendTransactionError() data class UnknownError(val ex: Exception? = null) : SendTransactionError() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 96b5db01fa..c6b5e9258c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -10,7 +10,6 @@ import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin @@ -133,8 +132,7 @@ class SendTransactionUseCase( val resource = tangemError.localizedDescriptionRes() val resId = resource.resId ?: R.string.common_unknown_error val resArgs = resource.args.map { it.value } - val textReference = resourceReference(resId, wrappedList(resArgs)) - SendTransactionError.TangemSdkError(tangemError.code, textReference) + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) } is BlockchainSdkError.WrappedTangemError -> { parseWrappedError(tangemError) // todo remove when sdk errors are revised diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d1cfaf3f7e..a411e3c160 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -96,7 +96,8 @@ platform :android do firebase_app_distribution( app: ENV['app_id_internal'], apk_path: ENV['apk_path_internal'], - groups: ENV['groups'] + groups: ENV['groups'], + release_notes: ENV['releaseNotes'] ) end end diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 865ab65f7b..b8d9e6465d 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics.models) implementation(projects.common.routing) + implementation(projects.common.ui) /* Project - Domain */ implementation(projects.domain.models) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 92b6f46de3..e2a4774961 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -17,7 +18,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { private val previewState = UserWalletListUM( userWallets = persistentListOf( - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), name = stringReference("My Wallet"), information = getInformation(3, "4 496,75 $"), @@ -25,7 +26,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_2".encodeToByteArray()), name = stringReference("Old wallet"), information = getInformation(3, "4 496,75 $"), @@ -33,7 +34,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { isEnabled = true, onClick = {}, ), - UserWalletListUM.UserWalletUM( + UserWalletItemUM( id = UserWalletId("user_wallet_3".encodeToByteArray()), name = stringReference("Multi Card"), information = getInformation(3, "4 496,75 $"), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index 1569f48efb..a8ef5eb141 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,25 +1,14 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList @Immutable internal data class UserWalletListUM( - val userWallets: ImmutableList, + val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, -) { - - @Immutable - data class UserWalletUM( - val id: UserWalletId, - val name: TextReference, - val information: TextReference, - val imageUrl: String, - val isEnabled: Boolean, - val onClick: () -> Unit, - ) -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 12f18cc1f4..a36e126509 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -1,12 +1,12 @@ package com.tangem.features.details.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.details.utils.UserWalletsFetcher @@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor( } private fun updateState( - userWallets: ImmutableList, + userWallets: ImmutableList, shouldSaveUserWallets: Boolean, isWalletSavingInProgress: Boolean, ) = state.update { value -> diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 794ef632c3..b09124ca04 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,7 +1,6 @@ package com.tangem.features.details.ui import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -10,32 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R -import com.tangem.features.details.ui.coil.RotationTransformation @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { BlockCard( modifier = modifier, ) { - state.userWallets.forEach { model -> - key(model.id) { + state.userWallets.forEach { state -> + key(state.id) { UserWalletItem( modifier = Modifier.fillMaxWidth(), - model = model, + state = state, ) } } @@ -47,96 +39,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M } } -@Composable -private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - onClick = model.onClick, - enabled = model.isEnabled, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size68) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Image(imageUrl = model.imageUrl) - NameAndInfo( - name = model.name, - information = model.information, - ) - } - } -} - -@Composable -private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { - Column( - modifier = modifier.heightIn(min = TangemTheme.dimens.size40), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - text = name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - AnimatedContent( - targetState = information.resolveReference(), - label = "User wallet information", - ) { information -> - Text( - text = information, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -private fun Image(imageUrl: String, modifier: Modifier = Modifier) { - val imageModifier = modifier - .width(TangemTheme.dimens.size24) - .height(TangemTheme.dimens.size36) - .clip(TangemTheme.shapes.roundedCornersSmall) - - SubcomposeAsyncImage( - modifier = imageModifier, - model = ImageRequest.Builder(LocalContext.current) - .transformations(RotationTransformation(angle = 90f)) - .size( - width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, - height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, - ) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { - RectangleShimmer( - modifier = imageModifier, - radius = TangemTheme.dimens.size2, - ) - }, - error = { - Image( - modifier = imageModifier, - painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36), - contentDescription = null, - ) - }, - contentDescription = null, - ) -} - @Composable private fun AddWalletButton( text: TextReference, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index a9782fc1d9..f51b383fbd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -1,13 +1,13 @@ package com.tangem.features.details.utils +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList @@ -19,7 +19,7 @@ internal fun List.toUiModels( balances: Map = emptyMap(), isLoading: Boolean = true, isBalancesHidden: Boolean = false, -): ImmutableList = this.map { model -> +): ImmutableList = this.map { model -> val balance = balances[model.walletId] model.toUiModel( @@ -37,7 +37,7 @@ private fun UserWallet.toUiModel( isLoading: Boolean, isBalanceHidden: Boolean, onClick: () -> Unit, -): UserWalletUM = UserWalletUM( +): UserWalletItemUM = UserWalletItemUM( id = walletId, name = stringReference(name), information = getInfo( @@ -59,7 +59,7 @@ private fun UserWallet.getInfo( ): TextReference { val dividerRef = stringReference(value = " • ") - val cardCount = getCardCount() + val cardCount = getCardsCount() ?: 1 val cardCountRef = TextReference.PluralRes( id = R.plurals.card_label_card_count, count = cardCount, @@ -99,12 +99,4 @@ private fun getBalanceInfo( } else { combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) } -} - -private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount.inc() - is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc() - is CardDTO.BackupStatus.NoBackup, - null, - -> 1 } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index e141c4034e..cd5013c005 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.utils import arrow.core.Either import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -22,7 +23,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -40,7 +40,7 @@ internal class UserWalletsFetcher @Inject constructor( ) { @OptIn(ExperimentalCoroutinesApi::class) - val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) combine( @@ -72,7 +72,7 @@ internal class UserWalletsFetcher @Inject constructor( maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - ): Lce> = lce { + ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, block = { maybeBalances.bindOrNull().orEmpty() }, diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index 761758d465..63a0f48e26 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -1,18 +1,15 @@ package com.tangem.features.managetokens.component -import androidx.compose.runtime.Composable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.wallets.models.UserWalletId -interface AddCustomTokenComponent { - - @Composable - fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) +interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, + val onDismiss: () -> Unit, ) - interface Factory { - fun create(params: Params): AddCustomTokenComponent - } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index a38a96267c..c327e0ac05 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -2,11 +2,11 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensComponent : ComposableContentComponent { - data class Params(val mode: Mode) + data class Params(val userWalletId: UserWalletId?) - enum class Mode { READ_ONLY, MANAGE, } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 1305e90261..a8d50aa900 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -22,8 +22,10 @@ dependencies { implementation(projects.core.featuretoggles) /* Project - Domain */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /* AndroidX */ implementation(deps.androidx.activity.compose) @@ -43,5 +45,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) + implementation(deps.decompose.ext.compose) implementation(deps.timber) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index acb8dc42a4..3f6d401d30 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -1,19 +1,22 @@ package com.tangem.features.managetokens.component -import androidx.compose.foundation.lazy.LazyListScope -import com.tangem.domain.tokens.model.Network +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork -internal interface CustomTokenFormComponent { - - fun content(scope: LazyListScope) +internal interface CustomTokenFormComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, - val networkId: Network.ID, + val network: SelectedNetwork, + val derivationPath: SelectedDerivationPath, + val formValues: CustomTokenFormValues, + val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, + val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, ) - interface Factory { - fun create(params: Params): CustomTokenFormComponent - } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt deleted file mode 100644 index a2551d430c..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.managetokens.component - -import androidx.compose.foundation.lazy.LazyListScope -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.managetokens.entity.SelectedNetworkUM - -internal interface CustomTokenNetworkSelectorComponent { - - fun content(scope: LazyListScope) - - data class Params( - val userWalletId: UserWalletId, - val selectedNetwork: SelectedNetworkUM?, - val onNetworkSelected: (SelectedNetworkUM) -> Unit, - ) - - interface Factory { - fun create(params: Params): CustomTokenNetworkSelectorComponent - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt new file mode 100644 index 0000000000..b3e4a159a0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenSelectorComponent.kt @@ -0,0 +1,27 @@ +package com.tangem.features.managetokens.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork + +internal interface CustomTokenSelectorComponent : ComposableContentComponent { + + sealed class Params { + + data class NetworkSelector( + val userWalletId: UserWalletId, + val selectedNetwork: SelectedNetwork?, + val onNetworkSelected: (SelectedNetwork) -> Unit, + ) : Params() + + data class DerivationPathSelector( + val userWalletId: UserWalletId, + val selectedDerivationPath: SelectedDerivationPath?, + val onDerivationPathSelected: (SelectedDerivationPath) -> Unit, + ) : Params() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt new file mode 100644 index 0000000000..dd0faa0556 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -0,0 +1,179 @@ +package com.tangem.features.managetokens.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.* +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAddCustomTokenComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: AddCustomTokenComponent.Params, + private val selectorComponentFactory: CustomTokenSelectorComponent.Factory, + private val formComponentFactory: CustomTokenFormComponent.Factory, +) : AddCustomTokenComponent, AppComponentContext by context { + + private val navigation = StackNavigation() + private val contentStack = childStack( + key = "add_custom_token_content_stack", + source = navigation, + initialConfiguration = AddCustomTokenConfig( + userWalletId = params.userWalletId, + step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, + popBack = ::dismiss, + ), + handleBackButton = true, + serializer = AddCustomTokenConfig.serializer(), + childFactory = ::contentChild, + ) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val config = remember { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = contentStack.active.configuration, + ) + } + val childStack by contentStack.subscribeAsState() + + AddCustomTokenBottomSheet( + config = config.copy( + content = childStack.active.configuration, + ), + content = { modifier -> + Children( + stack = childStack, + animation = stackAnimation(), + ) { child -> + child.instance.Content(modifier = modifier) + } + }, + ) + } + + private fun contentChild( + config: AddCustomTokenConfig, + componentContext: ComponentContext, + ): ComposableContentComponent = when (config.step) { + AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> { + selectorComponentFactory.create( + context = childByContext(componentContext), + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = config.userWalletId, + selectedNetwork = null, + onNetworkSelected = { network -> + showForm(network = network) + }, + ), + ) + } + AddCustomTokenConfig.Step.NETWORK_SELECTOR -> { + selectorComponentFactory.create( + context = childByContext(componentContext), + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = config.userWalletId, + selectedNetwork = config.selectedNetwork, + onNetworkSelected = { network -> + showForm(network = network) + }, + ), + ) + } + AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> { + selectorComponentFactory.create( + context = childByContext(componentContext), + params = CustomTokenSelectorComponent.Params.DerivationPathSelector( + userWalletId = config.userWalletId, + selectedDerivationPath = config.selectedDerivationPath, + onDerivationPathSelected = { derivationPath -> + showForm(derivationPath = derivationPath) + }, + ), + ) + } + AddCustomTokenConfig.Step.FORM -> { + formComponentFactory.create( + context = childByContext(componentContext), + params = CustomTokenFormComponent.Params( + userWalletId = config.userWalletId, + network = config.selectedNetwork ?: error("Network is not selected"), + derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath( + value = config.selectedNetwork.derivationPath, + name = resourceReference(R.string.custom_token_derivation_path_default), + ), + formValues = config.formValues, + onSelectNetworkClick = ::showNetworkSelector, + onSelectDerivationPathClick = ::showDerivationPathSelector, + ), + ) + } + } + + private fun showDerivationPathSelector(formValues: CustomTokenFormValues) { + val currentConfig = contentStack.value.active.configuration + + val config = currentConfig.copy( + step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR, + formValues = formValues, + popBack = navigation::pop, + ) + navigation.push(config) + } + + private fun showNetworkSelector(formValues: CustomTokenFormValues) { + val currentConfig = contentStack.value.active.configuration + + val config = currentConfig.copy( + step = AddCustomTokenConfig.Step.NETWORK_SELECTOR, + formValues = formValues, + popBack = navigation::pop, + ) + navigation.push(config) + } + + private fun showForm(network: SelectedNetwork? = null, derivationPath: SelectedDerivationPath? = null) { + val currentConfig = contentStack.value.active.configuration + + val config = currentConfig.copy( + step = AddCustomTokenConfig.Step.FORM, + selectedNetwork = network ?: currentConfig.selectedNetwork, + selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath, + popBack = ::dismiss, + ) + navigation.replaceAll(config) + } + + @AssistedFactory + interface Factory : AddCustomTokenComponent.Factory { + override fun create( + context: AppComponentContext, + params: AddCustomTokenComponent.Params, + ): DefaultAddCustomTokenComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt new file mode 100644 index 0000000000..982e4979e1 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenFormComponent.kt @@ -0,0 +1,161 @@ +package com.tangem.features.managetokens.component.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.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.CustomTokenFormContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal class DefaultCustomTokenFormComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: CustomTokenFormComponent.Params, +) : CustomTokenFormComponent, AppComponentContext by context { + + private val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by state.collectAsStateWithLifecycle() + + CustomTokenFormContent( + modifier = modifier, + model = state, + ) + } + + private fun getInitialState(): CustomTokenFormUM { + return CustomTokenFormUM( + networkName = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = params.network.name, + onClick = ::selectNetwork, + ), + tokenForm = getInitialTokenForm(), + derivationPath = ClickableFieldUM( + label = resourceReference(R.string.custom_token_derivation_path), + value = params.derivationPath.name, + onClick = ::selectDerivationPath, + ), + saveToken = { + // TODO: Save token: [REDACTED_JIRA] + }, + ) + } + + private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { + val formValues = params.formValues + + val form = CustomTokenFormUM.TokenFormUM( + contractAddress = TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(value = "0x000000000000000000000000000..."), + onValueChange = ::updateContractAddress, + ), + name = TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = resourceReference(R.string.custom_token_name_input_placeholder), + onValueChange = ::updateTokenName, + ), + symbol = TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), + onValueChange = ::updateTokenSymbol, + ), + decimals = TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(value = "0"), + onValueChange = ::updateDecimals, + ), + ) + + return formValues.fillValues(form) + } + + private fun updateContractAddress(value: String) { + // TODO: Add field validation: [REDACTED_JIRA] + state.update { state -> + val tokenForm = state.tokenForm ?: return@update state + + state.copy( + tokenForm = tokenForm.copy( + contractAddress = tokenForm.contractAddress.copy(value = value), + ), + ) + } + } + + private fun updateTokenName(value: String) { + // TODO: Add field validation: [REDACTED_JIRA] + state.update { state -> + val tokenForm = state.tokenForm ?: return@update state + + state.copy( + tokenForm = tokenForm.copy( + name = tokenForm.name.copy(value = value), + ), + ) + } + } + + private fun updateTokenSymbol(value: String) { + // TODO: Add field validation: [REDACTED_JIRA] + state.update { state -> + val tokenForm = state.tokenForm ?: return@update state + + state.copy( + tokenForm = tokenForm.copy( + symbol = tokenForm.symbol.copy(value = value), + ), + ) + } + } + + private fun updateDecimals(value: String) { + // TODO: Add field validation: [REDACTED_JIRA] + state.update { state -> + val tokenForm = state.tokenForm ?: return@update state + + state.copy( + tokenForm = tokenForm.copy( + decimals = tokenForm.decimals.copy(value = value), + ), + ) + } + } + + private fun selectNetwork() { + params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm)) + } + + private fun selectDerivationPath() { + params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) + } + + @AssistedFactory + interface Factory : CustomTokenFormComponent.Factory { + override fun create( + context: AppComponentContext, + params: CustomTokenFormComponent.Params, + ): DefaultCustomTokenFormComponent + } + + private companion object { + const val FORM_VALUES_KEY = "form_values" + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt new file mode 100644 index 0000000000..c748d7363f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenSelectorComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.managetokens.component.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.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.ui.CustomTokenSelectorContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow + +internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: CustomTokenSelectorComponent.Params, +) : CustomTokenSelectorComponent, AppComponentContext by context { + + private val state: MutableStateFlow = MutableStateFlow( + value = PreviewCustomTokenSelectorComponent(params = params).previewState, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by state.collectAsStateWithLifecycle() + + CustomTokenSelectorContent( + modifier = modifier, + model = state, + ) + } + + @AssistedFactory + interface Factory : CustomTokenSelectorComponent.Factory { + override fun create( + context: AppComponentContext, + params: CustomTokenSelectorComponent.Params, + ): DefaultCustomTokenSelectorComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index e89139cf5f..9d90bf8789 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -1,12 +1,21 @@ package com.tangem.features.managetokens.component.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig import com.tangem.features.managetokens.model.ManageTokensModel import com.tangem.features.managetokens.ui.ManageTokensScreen import dagger.assisted.Assisted @@ -16,18 +25,46 @@ import dagger.assisted.AssistedInject internal class DefaultManageTokensComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted params: ManageTokensComponent.Params, + private val addCustomTokenComponentFactory: AddCustomTokenComponent.Factory, ) : ManageTokensComponent, AppComponentContext by context { private val model: ManageTokensModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = BottomSheetConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + BackHandler(onBack = state.popBack) ManageTokensScreen( modifier = modifier, state = state, ) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: BottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is BottomSheetConfig.AddCustomToken -> { + addCustomTokenComponentFactory.create( + context = childByContext(componentContext), + params = AddCustomTokenComponent.Params( + userWalletId = config.userWalletId, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } } @AssistedFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index b428c0ed2d..a09d7b2b93 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -4,81 +4,72 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.AddCustomTokenComponent -import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent -import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM -import com.tangem.features.managetokens.entity.AddCustomTokenUM -import com.tangem.features.managetokens.entity.ClickableFieldUM -import com.tangem.features.managetokens.entity.SelectedNetworkUM -import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update internal class PreviewAddCustomTokenComponent( - initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(popBack = {}), + initialState: AddCustomTokenConfig = AddCustomTokenConfig( + userWalletId = UserWalletId(stringValue = "321"), + step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, + popBack = {}, + ), ) : AddCustomTokenComponent { - private val userWalletId = UserWalletId(stringValue = "321") + private val previewState: MutableStateFlow = MutableStateFlow(initialState) - private val previewState: MutableStateFlow = MutableStateFlow(initialState) + override fun dismiss() { + /* no-op */ + } @Composable - override fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) { + override fun BottomSheet() { val state by previewState.collectAsStateWithLifecycle() val config = TangemBottomSheetConfig( - isShow = isVisible, - onDismissRequest = onDismiss, + isShow = true, + onDismissRequest = ::dismiss, content = state, ) AddCustomTokenBottomSheet( config = config, - content = { - when (val s = state) { - is AddCustomTokenUM.Form -> { - PreviewCustomTokenFormComponent( - networkName = ClickableFieldUM( - label = resourceReference(R.string.custom_token_network_input_title), - value = stringReference(s.selectedNetwork.name), - onClick = { showNetworkSelector(s.selectedNetwork) }, + content = { modifier -> + when (state.step) { + AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> { + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = state.userWalletId, + selectedNetwork = null, + onNetworkSelected = {}, ), - ).content(this) + ).Content(modifier) } - is AddCustomTokenUM.NetworkSelector -> { - PreviewCustomTokenNetworkSelectorComponent( - params = CustomTokenNetworkSelectorComponent.Params( - userWalletId = userWalletId, - selectedNetwork = s.selectedNetwork, - onNetworkSelected = ::showForm, + AddCustomTokenConfig.Step.NETWORK_SELECTOR -> { + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = state.userWalletId, + selectedNetwork = state.selectedNetwork, + onNetworkSelected = {}, ), - networksSize = 20, - ).content(this) + ).Content(modifier) + } + AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> { + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.DerivationPathSelector( + userWalletId = state.userWalletId, + selectedDerivationPath = state.selectedDerivationPath, + onDerivationPathSelected = {}, + ), + ).Content(modifier) + } + AddCustomTokenConfig.Step.FORM -> { + PreviewCustomTokenFormComponent().Content(modifier) } } }, ) } - - private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) { - previewState.update { - AddCustomTokenUM.NetworkSelector(selectedNetwork, popBack = { showForm(selectedNetwork) }) - } - } - - private fun showForm(network: SelectedNetworkUM) { - previewState.update { - AddCustomTokenUM.Form( - popBack = {}, - selectedNetwork = network, - addTokenButton = AddCustomTokenButtonUM.Visible( - isEnabled = false, - onClick = {}, - ), - ) - } - } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt index 625090f28d..1c69d487de 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt @@ -1,81 +1,87 @@ package com.tangem.features.managetokens.component.preview -import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.features.managetokens.component.CustomTokenFormComponent -import com.tangem.features.managetokens.entity.ClickableFieldUM -import com.tangem.features.managetokens.entity.CustomTokenFormUM -import com.tangem.features.managetokens.entity.TextInputFieldUM +import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R -import com.tangem.features.managetokens.ui.customTokenFormContent +import com.tangem.features.managetokens.ui.CustomTokenFormContent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal class PreviewCustomTokenFormComponent( - networkName: ClickableFieldUM = ClickableFieldUM( - label = resourceReference(R.string.custom_token_network_input_title), - value = stringReference(value = "Ethereum"), - onClick = {}, - ), - derivationPath: ClickableFieldUM = ClickableFieldUM( - label = resourceReference(R.string.custom_token_derivation_path), - value = stringReference(value = "Default"), - onClick = {}, - ), + networkName: ClickableFieldUM = PreviewCustomTokenFormComponent.networkName, + derivationPath: ClickableFieldUM = PreviewCustomTokenFormComponent.derivationPath, canAddToken: Boolean = false, - contractAddress: TextInputFieldUM = TextInputFieldUM( - label = resourceReference(R.string.custom_token_contract_address_input_title), - placeholder = stringReference(value = "0x000000000000000000000000000"), - value = "", - onValueChange = {}, - ), - tokenName: TextInputFieldUM = TextInputFieldUM( - label = resourceReference(R.string.custom_token_name_input_title), - placeholder = stringReference(value = "E.g. USD Coin"), - value = "", - onValueChange = {}, - ), - tokenSymbol: TextInputFieldUM = TextInputFieldUM( - label = resourceReference(R.string.custom_token_token_symbol_input_title), - placeholder = stringReference(value = "E.g. USDC"), - value = "", - onValueChange = {}, - ), - tokenDecimals: TextInputFieldUM = TextInputFieldUM( - label = resourceReference(R.string.custom_token_decimals_input_title), - placeholder = stringReference(value = "8"), - value = "", - onValueChange = {}, - ), - notifications: ImmutableList = persistentListOf( - CustomTokenFormUM.NotificationUM( - id = "1", - config = NotificationConfig( - title = stringReference(value = "Note that tokens can be created by anyone"), - subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"), - iconResId = R.drawable.img_attention_20, - ), - ), - ), + tokenForm: CustomTokenFormUM.TokenFormUM? = PreviewCustomTokenFormComponent.tokenForm, + notifications: ImmutableList = PreviewCustomTokenFormComponent.notifications, ) : CustomTokenFormComponent { private val previewState = CustomTokenFormUM( networkName = networkName, - contractAddress = contractAddress, - tokenName = tokenName, - tokenSymbol = tokenSymbol, - tokenDecimals = tokenDecimals, + tokenForm = tokenForm, derivationPath = derivationPath, notifications = notifications, canAddToken = canAddToken, - onDerivationPathClick = {}, - onNetworkClick = {}, - onAddClick = {}, + saveToken = {}, ) - override fun content(scope: LazyListScope) { - scope.customTokenFormContent(model = previewState) + @Composable + override fun Content(modifier: Modifier) { + CustomTokenFormContent(modifier = modifier, model = previewState) + } + + companion object { + val networkName: ClickableFieldUM = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = stringReference(value = "Ethereum"), + onClick = {}, + ) + val derivationPath: ClickableFieldUM = ClickableFieldUM( + label = resourceReference(R.string.custom_token_derivation_path), + value = stringReference(value = "Default"), + onClick = {}, + ) + val tokenForm: CustomTokenFormUM.TokenFormUM = CustomTokenFormUM.TokenFormUM( + contractAddress = TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(value = "0x000000000000000000000000000"), + value = "", + onValueChange = {}, + ), + name = TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = stringReference(value = "E.g. USD Coin"), + value = "", + onValueChange = {}, + ), + symbol = TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = stringReference(value = "E.g. USDC"), + value = "", + onValueChange = {}, + ), + decimals = TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(value = "8"), + value = "", + onValueChange = {}, + ), + ) + val notifications: ImmutableList = persistentListOf( + CustomTokenFormUM.NotificationUM( + id = "1", + config = NotificationConfig( + title = stringReference(value = "Note that tokens can be created by anyone"), + subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"), + iconResId = R.drawable.img_attention_20, + ), + ), + ) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt deleted file mode 100644 index 620dbe1222..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.features.managetokens.component.preview - -import androidx.compose.foundation.lazy.LazyListScope -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent -import com.tangem.features.managetokens.entity.CurrencyNetworkUM -import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM -import com.tangem.features.managetokens.entity.SelectedNetworkUM -import com.tangem.features.managetokens.impl.R -import com.tangem.features.managetokens.ui.customTokenNetworkSelectorContent -import kotlinx.collections.immutable.toImmutableList - -internal class PreviewCustomTokenNetworkSelectorComponent( - private val params: CustomTokenNetworkSelectorComponent.Params = CustomTokenNetworkSelectorComponent.Params( - userWalletId = UserWalletId(stringValue = "321"), - selectedNetwork = null, - onNetworkSelected = {}, - ), - networksSize: Int = 5, -) : CustomTokenNetworkSelectorComponent { - - private val previewNetworks = List(size = networksSize) { networkIndex -> - val n = SelectedNetworkUM( - id = Network.ID(networkIndex.toString()), - name = "Network $networkIndex", - ) - - CurrencyNetworkUM( - id = n.id, - name = n.name, - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = n.id == params.selectedNetwork?.id, - onSelectedStateChange = { params.onNetworkSelected(n) }, - ) - }.toImmutableList() - - private val previewState = CustomTokenNetworkSelectorUM( - showTitle = params.selectedNetwork == null, - networks = previewNetworks, - ) - - override fun content(scope: LazyListScope) { - scope.customTokenNetworkSelectorContent( - model = previewState, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt new file mode 100644 index 0000000000..1e773cc319 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -0,0 +1,79 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.CustomTokenSelectorContent +import kotlinx.collections.immutable.toImmutableList + +internal class PreviewCustomTokenSelectorComponent( + private val params: Params = Params.NetworkSelector( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = null, + onNetworkSelected = {}, + ), + itemsSize: Int = 5, +) : CustomTokenSelectorComponent { + + private val previewItems = List(size = itemsSize) { index -> + when (params) { + is Params.DerivationPathSelector -> { + val d = SelectedDerivationPath( + value = "m/44'/0'/0'/0/$index", + name = stringReference(value = "Network $index"), + ) + + DerivationPathUM( + value = "m/44'/0'/0'/0/$index", + blockchainName = d.name, + isSelected = d.value == params.selectedDerivationPath?.value, + onSelectedStateChange = { params.onDerivationPathSelected(d) }, + ) + } + is Params.NetworkSelector -> { + val n = SelectedNetwork( + id = Network.ID(index.toString()), + name = stringReference(value = "Network $index"), + derivationPath = "m/44'/0'/0'/0/$index", + ) + + CurrencyNetworkUM( + networkId = n.id, + name = "Network $index", + type = "N$index", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = n.id == params.selectedNetwork?.id, + onSelectedStateChange = { params.onNetworkSelected(n) }, + ) + } + } + }.toImmutableList() + + val previewState = CustomTokenSelectorUM( + header = when (params) { + is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton({}) + is Params.NetworkSelector -> if (params.selectedNetwork == null) { + CustomTokenSelectorUM.HeaderUM.Description + } else { + CustomTokenSelectorUM.HeaderUM.None + } + }, + items = previewItems, + ) + + @Composable + override fun Content(modifier: Modifier) { + CustomTokenSelectorContent(modifier = modifier, model = previewState) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 980afcb895..efa89d5b8e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -9,11 +9,14 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen import kotlinx.collections.immutable.mutate @@ -47,8 +50,10 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { onActiveChange = ::toggleSearchBar, ), hasChanges = false, - isLoading = false, - onSaveClick = {}, + isInitialBatchLoading = false, + isNextBatchLoading = true, + loadMore = { false }, + saveChanges = {}, ), ) @@ -58,7 +63,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { initItems() } else { items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) + currency.name.contains(query, ignoreCase = true) }.toPersistentList() } @@ -96,34 +101,28 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { }.toPersistentList() private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Custom token $index", + symbol = "CT$index", + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + topBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, ), onRemoveClick = {}, ) private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, + id = ManagedCryptoCurrency.ID(index.toString()), + name = "Currency $index", + symbol = "C$index", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, ), networks = if (index == 2) { CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) @@ -135,7 +134,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( - id = Network.ID(networkIndex.toString()), + networkId = Network.ID(networkIndex.toString()), name = "NETWORK$networkIndex", type = "N$networkIndex", iconResId = R.drawable.ic_eth_16, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt index f84b3c4c8e..9de5afa38c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt @@ -1,6 +1,12 @@ package com.tangem.features.managetokens.di +import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.impl.DefaultAddCustomTokenComponent +import com.tangem.features.managetokens.component.impl.DefaultCustomTokenFormComponent +import com.tangem.features.managetokens.component.impl.DefaultCustomTokenSelectorComponent import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent import dagger.Binds import dagger.Module @@ -15,4 +21,22 @@ internal interface ComponentModule { @Binds @Singleton fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory + + @Binds + @Singleton + fun bindAddCustomTokenComponentFactory( + factory: DefaultAddCustomTokenComponent.Factory, + ): AddCustomTokenComponent.Factory + + @Binds + @Singleton + fun bindCustomTokenSelectorComponentFactory( + factory: DefaultCustomTokenSelectorComponent.Factory, + ): CustomTokenSelectorComponent.Factory + + @Binds + @Singleton + fun bindCustomTokenFormComponentFactory( + factory: DefaultCustomTokenFormComponent.Factory, + ): CustomTokenFormComponent.Factory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt deleted file mode 100644 index a34939d61f..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.features.managetokens.entity - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.domain.tokens.model.Network - -@Immutable -internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent { - - abstract val selectedNetwork: SelectedNetworkUM? - abstract val addTokenButton: AddCustomTokenButtonUM - - abstract val popBack: () -> Unit - - data class NetworkSelector( - override val selectedNetwork: SelectedNetworkUM? = null, - override val popBack: () -> Unit, - ) : AddCustomTokenUM() { - - override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden - } - - data class Form( - override val selectedNetwork: SelectedNetworkUM, - override val addTokenButton: AddCustomTokenButtonUM.Visible, - override val popBack: () -> Unit, - ) : AddCustomTokenUM() -} - -@Immutable -internal data class SelectedNetworkUM( - val id: Network.ID, - val name: String, -) - -@Immutable -internal sealed class AddCustomTokenButtonUM { - - open val onClick: () -> Unit = {} - - open val isEnabled: Boolean = false - - val isVisible: Boolean - get() = this is Visible - - data object Hidden : AddCustomTokenButtonUM() { - override val onClick: () -> Unit = {} - } - - data class Visible( - override val isEnabled: Boolean, - override val onClick: () -> Unit, - ) : AddCustomTokenButtonUM() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt deleted file mode 100644 index 0cfc505545..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.features.managetokens.entity - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.rows.model.ChainRowUM -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed class CurrencyItemUM { - - abstract val id: String - abstract val model: ChainRowUM - - data class Basic( - override val id: String, - override val model: ChainRowUM, - val networks: NetworksUM, - val onExpandClick: () -> Unit, - ) : CurrencyItemUM() { - - @Immutable - sealed class NetworksUM { - - data object Collapsed : NetworksUM() - - data class Expanded( - val networks: ImmutableList, - ) : NetworksUM() - } - } - - data class Custom( - override val id: String, - override val model: ChainRowUM, - val onRemoveClick: () -> Unit, - ) : CurrencyItemUM() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt deleted file mode 100644 index 477585db3f..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.managetokens.entity - -import androidx.compose.runtime.Immutable -import com.tangem.domain.tokens.model.Network - -@Immutable -internal data class CurrencyNetworkUM( - val id: Network.ID, - val name: String, - val type: String, - val iconResId: Int, - val isMainNetwork: Boolean, - val isSelected: Boolean, - val onSelectedStateChange: (Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt deleted file mode 100644 index 1435d25f7d..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.managetokens.entity - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal data class CustomTokenNetworkSelectorUM( - val showTitle: Boolean, - val networks: ImmutableList, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt deleted file mode 100644 index d53e7e5e4d..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.managetokens.entity - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed class ManageTokensUM { - - abstract val popBack: () -> Unit - abstract val isLoading: Boolean - abstract val items: ImmutableList - abstract val topBar: ManageTokensTopBarUM - abstract val search: SearchBarUM - - data class ReadContent( - override val popBack: () -> Unit, - override val isLoading: Boolean, - override val items: ImmutableList, - override val topBar: ManageTokensTopBarUM, - override val search: SearchBarUM, - ) : ManageTokensUM() - - data class ManageContent( - override val popBack: () -> Unit, - override val isLoading: Boolean, - override val items: ImmutableList, - override val topBar: ManageTokensTopBarUM, - override val search: SearchBarUM, - val onSaveClick: () -> Unit, - val hasChanges: Boolean, - ) : ManageTokensUM() - - fun copySealed( - search: SearchBarUM = this.search, - items: ImmutableList = this.items, - hasChanges: Boolean = this is ManageContent && this.hasChanges, - ): ManageTokensUM { - return when (this) { - is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges) - is ReadContent -> copy(search = search, items = items) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt new file mode 100644 index 0000000000..417cd15585 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -0,0 +1,38 @@ +package com.tangem.features.managetokens.entity.customtoken + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal data class AddCustomTokenConfig( + val step: Step, + val popBack: () -> Unit, + val userWalletId: UserWalletId, + val selectedNetwork: SelectedNetwork? = null, + val selectedDerivationPath: SelectedDerivationPath? = null, + val formValues: CustomTokenFormValues = CustomTokenFormValues(), +) : TangemBottomSheetConfigContent { + + enum class Step { + INITIAL_NETWORK_SELECTOR, + NETWORK_SELECTOR, + DERIVATION_PATH_SELECTOR, + FORM, + } +} + +@Serializable +internal data class SelectedNetwork( + val id: Network.ID, + val name: TextReference, + val derivationPath: String, +) + +@Serializable +internal data class SelectedDerivationPath( + val value: String, + val name: TextReference, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt similarity index 58% rename from features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt rename to features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index ea9bfbceb0..902c7de226 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -1,42 +1,40 @@ -package com.tangem.features.managetokens.entity +package com.tangem.features.managetokens.entity.customtoken -import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf -@Immutable internal data class CustomTokenFormUM( val networkName: ClickableFieldUM, - val contractAddress: TextInputFieldUM, - val tokenName: TextInputFieldUM, - val tokenSymbol: TextInputFieldUM, - val tokenDecimals: TextInputFieldUM, val derivationPath: ClickableFieldUM, - val notifications: ImmutableList, - val canAddToken: Boolean, - val onNetworkClick: () -> Unit, - val onDerivationPathClick: () -> Unit, - val onAddClick: () -> Unit, + val tokenForm: TokenFormUM?, + val notifications: ImmutableList = persistentListOf(), + val canAddToken: Boolean = false, + val saveToken: () -> Unit, ) { - @Immutable + data class TokenFormUM( + val contractAddress: TextInputFieldUM, + val name: TextInputFieldUM, + val symbol: TextInputFieldUM, + val decimals: TextInputFieldUM, + ) + data class NotificationUM( val id: String, val config: NotificationConfig, ) } -@Immutable internal data class TextInputFieldUM( val label: TextReference, val placeholder: TextReference, - val value: String, - val onValueChange: (String) -> Unit, + val value: String = "", val error: TextReference? = null, + val onValueChange: (String) -> Unit, ) -@Immutable internal data class ClickableFieldUM( val label: TextReference, val value: TextReference, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt new file mode 100644 index 0000000000..1551b4bc30 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt @@ -0,0 +1,31 @@ +package com.tangem.features.managetokens.entity.customtoken + +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM +import kotlinx.serialization.Serializable + +@JvmInline +@Serializable +internal value class CustomTokenFormValues private constructor(private val values: List) { + + constructor() : this(values = emptyList()) + + constructor(form: TokenFormUM?) : this( + values = if (form == null) { + emptyList() + } else { + listOf( + form.contractAddress.value, + form.name.value, + form.symbol.value, + form.decimals.value, + ) + }, + ) + + fun fillValues(to: TokenFormUM): TokenFormUM = to.copy( + contractAddress = to.contractAddress.copy(value = values.getOrElse(index = 0) { "" }), + name = to.name.copy(value = values.getOrElse(index = 1) { "" }), + symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }), + decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt new file mode 100644 index 0000000000..25c08fac10 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenSelectorUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.managetokens.entity.customtoken + +import androidx.compose.runtime.Immutable +import com.tangem.features.managetokens.entity.item.SelectableItemUM +import kotlinx.collections.immutable.ImmutableList + +internal data class CustomTokenSelectorUM( + val header: HeaderUM, + val items: ImmutableList, +) { + + @Immutable + sealed class HeaderUM { + + data object None : HeaderUM() + + data object Description : HeaderUM() + + data class CustomDerivationButton( + val onClick: () -> Unit, + ) : HeaderUM() + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt new file mode 100644 index 0000000000..b61c576c2f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyItemUM.kt @@ -0,0 +1,43 @@ +package com.tangem.features.managetokens.entity.item + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class CurrencyItemUM { + + abstract val id: ManagedCryptoCurrency.ID + abstract val name: String + abstract val symbol: String + abstract val icon: CurrencyIconState + + data class Basic( + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, + val networks: NetworksUM, + val onExpandClick: () -> Unit, + ) : CurrencyItemUM() { + + @Immutable + sealed class NetworksUM { + + data object Collapsed : NetworksUM() + + data class Expanded( + val networks: ImmutableList, + ) : NetworksUM() + } + } + + data class Custom( + override val id: ManagedCryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val icon: CurrencyIconState, + val onRemoveClick: () -> Unit, + ) : CurrencyItemUM() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt new file mode 100644 index 0000000000..84f3aabb87 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.entity.item + +import com.tangem.domain.tokens.model.Network + +internal data class CurrencyNetworkUM( + val networkId: Network.ID, + val name: String, + val type: String, + val iconResId: Int, + val isMainNetwork: Boolean, + override val isSelected: Boolean, + override val onSelectedStateChange: (Boolean) -> Unit, +) : SelectableItemUM { + + override val id: String = networkId.value +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt new file mode 100644 index 0000000000..2190c50e4f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/DerivationPathUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.entity.item + +import com.tangem.core.ui.extensions.TextReference + +internal data class DerivationPathUM( + val value: String, + val blockchainName: TextReference, + override val isSelected: Boolean, + override val onSelectedStateChange: (Boolean) -> Unit, +) : SelectableItemUM { + + override val id: String = value +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/SelectableItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/SelectableItemUM.kt new file mode 100644 index 0000000000..73c5910af8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/SelectableItemUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.managetokens.entity.item + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface SelectableItemUM { + + val id: String + val isSelected: Boolean + val onSelectedStateChange: (Boolean) -> Unit +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt new file mode 100644 index 0000000000..102140a57c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/BottomSheetConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.features.managetokens.entity.managetokens + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class BottomSheetConfig { + + @Serializable + data class AddCustomToken( + val userWalletId: UserWalletId, + ) : BottomSheetConfig() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensTopBarUM.kt similarity index 91% rename from features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt rename to features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensTopBarUM.kt index b2f08a0ef0..875ed483ed 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensTopBarUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.managetokens.entity +package com.tangem.features.managetokens.entity.managetokens import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt new file mode 100644 index 0000000000..4fc9d627c7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -0,0 +1,64 @@ +package com.tangem.features.managetokens.entity.managetokens + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class ManageTokensUM { + + abstract val popBack: () -> Unit + abstract val isInitialBatchLoading: Boolean + abstract val isNextBatchLoading: Boolean + abstract val items: ImmutableList + abstract val topBar: ManageTokensTopBarUM + abstract val search: SearchBarUM + abstract val loadMore: () -> Boolean + + data class ReadContent( + override val popBack: () -> Unit, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, + override val items: ImmutableList, + override val topBar: ManageTokensTopBarUM, + override val search: SearchBarUM, + override val loadMore: () -> Boolean, + ) : ManageTokensUM() + + data class ManageContent( + override val popBack: () -> Unit, + override val isInitialBatchLoading: Boolean, + override val isNextBatchLoading: Boolean, + override val items: ImmutableList, + override val topBar: ManageTokensTopBarUM, + override val search: SearchBarUM, + override val loadMore: () -> Boolean, + val saveChanges: () -> Unit, + val hasChanges: Boolean, + ) : ManageTokensUM() + + fun copySealed( + search: SearchBarUM = this.search, + items: ImmutableList = this.items, + hasChanges: Boolean = this is ManageContent && this.hasChanges, + isInitialBatchLoading: Boolean = this.isInitialBatchLoading, + isNextBatchLoading: Boolean = this.isNextBatchLoading, + ): ManageTokensUM { + return when (this) { + is ManageContent -> copy( + search = search, + items = items, + hasChanges = hasChanges, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) + is ReadContent -> copy( + search = search, + items = items, + isInitialBatchLoading = isInitialBatchLoading, + isNextBatchLoading = isNextBatchLoading, + ) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 6141538abd..941fdad919 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -1,53 +1,82 @@ package com.tangem.features.managetokens.model -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.util.fastForEachIndexed +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate 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.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.Network +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.* +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.entity.managetokens.BottomSheetConfig +import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.list.ChangedCurrencies +import com.tangem.features.managetokens.utils.list.ManageTokensListManager +import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped internal class ManageTokensModel @Inject constructor( - paramsContainer: ParamsContainer, - private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val manageTokensListManager: ManageTokensListManager, + private val messageSender: UiMessageSender, + paramsContainer: ParamsContainer, ) : Model() { private val params: ManageTokensComponent.Params = paramsContainer.require() - private val changedItemsIds: MutableSet = mutableSetOf() - private var items = initItems() - val state: MutableStateFlow = MutableStateFlow(value = getInitialState(mode = params.mode)) + val state: MutableStateFlow = MutableStateFlow(getInitialState(params.userWalletId)) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM { - return when (mode) { - ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel() - ManageTokensComponent.Mode.MANAGE -> createManageContentModel() + init { + manageTokensListManager.uiItems + .onEach { items -> updateItems(items) } + .launchIn(modelScope) + + manageTokensListManager.paginationStatus + .onEach { status -> updatePaginationStatus(status) } + .launchIn(modelScope) + + combine( + manageTokensListManager.currenciesToAdd, + manageTokensListManager.currenciesToRemove, + ::updateChangedItems, + ).launchIn(modelScope) + + modelScope.launch { + manageTokensListManager.launchPagination(params.userWalletId) + } + } + + private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { + return if (userWalletId == null) { + createReadContentModel() + } else { + createManageContentModel() } } private fun createReadContentModel(): ManageTokensUM.ReadContent { return ManageTokensUM.ReadContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_search_tokens), onBackButtonClick = router::pop, @@ -59,20 +88,22 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), + loadMore = ::loadMoreItems, ) } private fun createManageContentModel(): ManageTokensUM.ManageContent { return ManageTokensUM.ManageContent( popBack = router::pop, - isLoading = false, - items = initItems(), + isInitialBatchLoading = true, + isNextBatchLoading = false, + items = getInitialItems(), topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, endButton = TopAppBarButtonUM( iconRes = R.drawable.ic_plus_24, - onIconClicked = ::onAddCustomToken, + onIconClicked = ::navigateToAddCustomToken, ), ), search = SearchBarUM( @@ -82,162 +113,125 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), - onSaveClick = ::onSaveClick, hasChanges = false, + saveChanges = ::saveChanges, + loadMore = ::loadMoreItems, ) } - private fun onAddCustomToken() { - // TODO: [REDACTED_JIRA] - } - - private fun onSaveClick() { - // TODO: [REDACTED_JIRA] - } - - @Suppress("UnusedPrivateMember") - private fun searchCurrencies(query: String) { - // TODO: [REDACTED_JIRA] - val newItems = if (query.isBlank()) { - initItems() - } else { - state.value.items.filter { currency -> - currency.model.name.contains(query, ignoreCase = true) - }.toPersistentList() - } + private fun updateItems(items: ImmutableList) { state.update { state -> - state.copySealed(search = state.search.copy(query = query), items = newItems) + state.copySealed( + items = items, + ) + } + } + + private fun updatePaginationStatus(status: PaginationStatus<*>) { + state.update { state -> + when (status) { + is PaginationStatus.None, + is PaginationStatus.InitialLoading, + -> { + if (state.search.isActive) { + state + } else { + state.copySealed( + isInitialBatchLoading = true, + ) + } + } + is PaginationStatus.NextBatchLoading -> state.copySealed( + isNextBatchLoading = true, + ) + is PaginationStatus.InitialLoadingError -> { + val message = SnackbarMessage( + message = status.throwable.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + is PaginationStatus.Paginating, + is PaginationStatus.EndOfPagination, + -> state.copySealed( + isInitialBatchLoading = false, + isNextBatchLoading = false, + ) + } + } + } + + private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { + state.update { state -> + state.copySealed( + hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), + ) + } + } + + private fun loadMoreItems(): Boolean { + val state = state.value + if (state.isInitialBatchLoading || state.isNextBatchLoading) return false + + modelScope.launch { + manageTokensListManager.loadMore( + userWalletId = params.userWalletId, + query = state.search.query, + ) + } + + return true + } + + private fun getInitialItems(): ImmutableList { + return persistentListOf() + } + + private fun navigateToAddCustomToken() { + params.userWalletId?.let { + bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it)) + } + } + + private fun saveChanges() { + // TODO: [REDACTED_JIRA] + } + + private fun searchCurrencies(query: String) { + state.update { state -> + state.copySealed( + search = state.search.copy( + query = query, + isActive = true, + ), + ) + } + + modelScope.launch { + manageTokensListManager.search(params.userWalletId, query) } } private fun toggleSearchBar(isActive: Boolean) { state.update { state -> state.copySealed( - search = state.search.copy(isActive = isActive), + search = state.search.copy( + query = if (isActive) state.search.query else "", + isActive = isActive, + ), ) } - } - private fun initItems() = List(size = 30) { index -> - if (index < 2) { - getCustomItem(index) - } else { - getBasicItem(index) - } - }.toPersistentList() - - private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = "Custom token $index", - type = "CT$index", - icon = CurrencyIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - topBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, - ), - onRemoveClick = {}, - ) - - private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = "Currency $index", - type = "C$index", - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, - ), - networks = if (index == 2) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - onExpandClick = { toggleCurrency(index) }, - ) - - private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> - CurrencyNetworkUM( - id = Network.ID(networkIndex.toString()), - name = "NETWORK$networkIndex", - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = networkIndex == 0, - isSelected = false, - onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, - ) - }.toImmutableList() - - private fun toggleCurrency(index: Int) { - val updatedItem = when (val item = items[index]) { - is CurrencyItemUM.Basic -> item.copy( - networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { - CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) - } else { - CurrencyItemUM.Basic.NetworksUM.Collapsed - }, - ) - is CurrencyItemUM.Custom -> return - } - - state.update { state -> - items = items.mutate { - it[index] = updatedItem + modelScope.launch { + if (!isActive) { + manageTokensListManager.reload(params.userWalletId) } - state.copySealed(items = items) - } - } - - private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { - val updatedItem = when (val item = items[currencyIndex]) { - is CurrencyItemUM.Basic -> { - val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) - ?.copy( - networks = item.networks.networks.toPersistentList().mutate { - it.fastForEachIndexed { index, network -> - if (index == networkIndex) { - it[index] = network.copy( - iconResId = if (isSelected) { - R.drawable.img_eth_22 - } else { - R.drawable.ic_eth_16 - }, - isSelected = isSelected, - ) - } - } - }, - ) - ?: return - - item.copy(networks = updatedNetworks) - } - is CurrencyItemUM.Custom -> return - } - - val id = "${currencyIndex}_$networkIndex" - if (changedItemsIds.contains(id)) { - changedItemsIds.remove(id) - } else { - changedItemsIds.add(id) - } - - state.update { state -> - items = items.mutate { - it[currencyIndex] = updatedItem - } - state.copySealed( - items = items, - hasChanges = changedItemsIds.isNotEmpty(), - ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index a002b54b4c..b1b3cc0f0a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -1,146 +1,80 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle -import com.tangem.core.ui.components.isOpened -import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent -import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM -import com.tangem.features.managetokens.entity.AddCustomTokenUM -import com.tangem.features.managetokens.entity.SelectedNetworkUM +import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork import com.tangem.features.managetokens.impl.R @Composable -internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) { - TangemBottomSheet( +internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) { + TangemBottomSheet( config = config, + addBottomInsets = false, title = { model -> Title(model) }, containerColor = TangemTheme.colors.background.secondary, - content = { model -> - Content( - model = model, - content = content, - ) + content = { + val contentModifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxSize() + + content(contentModifier) }, ) } @Composable -private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) { - val showTokenNetworkTitle = model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null - - if (showTokenNetworkTitle) { - TangemTopAppBar( - modifier = modifier, - title = resourceReference(R.string.custom_token_network_selector_title), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(model.popBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - } else { - TangemBottomSheetTitle( - modifier = modifier, - title = resourceReference(R.string.add_custom_token_title), - ) - } -} - -@Composable -private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) { - val density = LocalDensity.current - val keyboardState by keyboardAsState() - - var fabHeight by remember { mutableStateOf(0.dp) } - - Scaffold( - modifier = modifier.imePadding(), - containerColor = TangemTheme.colors.background.secondary, - floatingActionButtonPosition = FabPosition.Center, - floatingActionButton = { - AnimatedVisibility( - modifier = Modifier.onSizeChanged { - fabHeight = with(density) { it.height.toDp() } - }, - visible = model.addTokenButton.isVisible && !keyboardState.isOpened, - enter = fadeIn(), - exit = fadeOut(), - label = "Add button visibility", - ) { - PrimaryButton( - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing16) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.custom_token_add_token), - enabled = model.addTokenButton.isEnabled, - onClick = model.addTokenButton.onClick, - ) - } - }, - ) { paddingValues -> - LazyColumn( - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing32 + fabHeight, - ), - ) { - item { - if (model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null) { - Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing12)) - } else { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), - contentAlignment = Alignment.Center, - ) { - Text( - modifier = Modifier.fillMaxWidth(fraction = 0.7f), - text = stringResource(id = R.string.custom_token_subtitle), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - } - } - } - - content() +private fun Title(model: AddCustomTokenConfig, modifier: Modifier = Modifier) { + when (model.step) { + AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, + AddCustomTokenConfig.Step.FORM, + -> { + TangemBottomSheetTitle( + modifier = modifier, + title = resourceReference(R.string.add_custom_token_title), + ) + } + AddCustomTokenConfig.Step.NETWORK_SELECTOR -> { + TangemTopAppBar( + modifier = modifier, + title = resourceReference(R.string.custom_token_network_selector_title), + titleAlignment = Alignment.CenterHorizontally, + startButton = TopAppBarButtonUM.Back(model.popBack), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) + } + AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> { + TangemTopAppBar( + modifier = modifier, + title = resourceReference(R.string.custom_token_derivation_path), + titleAlignment = Alignment.CenterHorizontally, + startButton = TopAppBarButtonUM.Back(model.popBack), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) } } } @@ -153,7 +87,7 @@ private fun Preview_AddCustomTokenBottomSheet( @PreviewParameter(AddCustomTokenComponentPreviewProvider::class) component: AddCustomTokenComponent, ) { TangemThemePreview { - component.BottomSheet(isVisible = true, onDismiss = {}) + component.BottomSheet() } } @@ -162,24 +96,37 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< get() = sequenceOf( PreviewAddCustomTokenComponent(), PreviewAddCustomTokenComponent( - initialState = AddCustomTokenUM.NetworkSelector( + initialState = AddCustomTokenConfig( + userWalletId = UserWalletId(stringValue = "321"), + step = AddCustomTokenConfig.Step.FORM, popBack = {}, - selectedNetwork = SelectedNetworkUM( - id = Network.ID(value = "0"), - name = "Ethereum", + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "1"), + name = stringReference("Ethereum"), + derivationPath = "m/44'/0'/0'/0/0", ), ), ), PreviewAddCustomTokenComponent( - initialState = AddCustomTokenUM.Form( + initialState = AddCustomTokenConfig( + userWalletId = UserWalletId(stringValue = "321"), + step = AddCustomTokenConfig.Step.NETWORK_SELECTOR, popBack = {}, - selectedNetwork = SelectedNetworkUM( - id = Network.ID(value = "1"), - name = "Ethereum", + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = stringReference("Ethereum"), + derivationPath = "m/44'/0'/0'/0/0", ), - addTokenButton = AddCustomTokenButtonUM.Visible( - isEnabled = false, - onClick = {}, + ), + ), + PreviewAddCustomTokenComponent( + initialState = AddCustomTokenConfig( + userWalletId = UserWalletId(stringValue = "321"), + step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR, + popBack = {}, + selectedDerivationPath = SelectedDerivationPath( + value = "m/44'/0'/0'/0/0", + name = stringReference("Ethereum"), ), ), ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index e898e44150..76ae892e76 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -2,97 +2,152 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.isOpened +import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent -import com.tangem.features.managetokens.entity.ClickableFieldUM -import com.tangem.features.managetokens.entity.CustomTokenFormUM -import com.tangem.features.managetokens.entity.TextInputFieldUM +import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription -internal fun LazyListScope.customTokenFormContent(model: CustomTokenFormUM) { - item { - ClickableField( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - model = model.networkName, - ) - } +@Composable +internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) { + val keyboard by keyboardAsState() + val bottomBarHeight by animateDpAsState( + label = "Bottom bar height", + targetValue = if (keyboard.isOpened) { + TangemTheme.dimens.spacing0 + } else { + with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + }, + ) + + Box( + modifier = modifier + .imePadding() + .fillMaxSize() + .background(color = TangemTheme.colors.background.secondary), + ) { + val scrollState = rememberScrollState() - item { Column( modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12) - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), + .verticalScroll(scrollState) + .fillMaxSize() + .padding(bottom = TangemTheme.dimens.spacing76), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - TextField( - model = model.contractAddress, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = model.tokenName, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = model.tokenSymbol, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = model.tokenDecimals, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Decimal, - imeAction = ImeAction.Next, - ), + AddCustomTokenDescription() + FormContent(model) + } + + PrimaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight) + .fillMaxWidth(), + text = stringResource(id = R.string.custom_token_add_token), + enabled = model.canAddToken, + onClick = model.saveToken, + ) + } +} + +@Composable +private fun FormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + ClickableField( + model = model.networkName, + ) + + val tokenForm = model.tokenForm + if (tokenForm != null) { + TokenForm(tokenForm) + } + + ClickableField( + model = model.derivationPath, + ) + + model.notifications.fastForEach { notification -> + Notification( + config = notification.config, + containerColor = TangemTheme.colors.button.disabled, ) } } +} - item { - ClickableField( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - model = model.derivationPath, +@Composable +private fun TokenForm(tokenForm: CustomTokenFormUM.TokenFormUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + TextField( + model = tokenForm.contractAddress, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), ) - } - - items( - items = model.notifications, - key = { it.id }, - ) { notification -> - Notification( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - config = notification.config, - containerColor = TangemTheme.colors.button.disabled, + TextField( + model = tokenForm.name, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), + ) + TextField( + model = tokenForm.symbol, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), + ) + TextField( + model = tokenForm.decimals, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Next, + ), ) } } @@ -124,7 +179,9 @@ private fun TextField( }, content = { SimpleTextField( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .fillMaxWidth(), value = model.value, onValueChange = model.onValueChange, readOnly = false, @@ -170,9 +227,7 @@ private fun Preview_CustomTokenFormContent( component: PreviewCustomTokenFormComponent, ) { TangemThemePreview { - LazyColumn( - modifier = Modifier.background(color = TangemTheme.colors.background.secondary), - ) { component.content(scope = this) } + component.Content(modifier = Modifier) } } @@ -183,14 +238,19 @@ private class PreviewCustomTokenFormComponentProvider : get() = sequenceOf( PreviewCustomTokenFormComponent(), PreviewCustomTokenFormComponent( - contractAddress = TextInputFieldUM( - label = stringReference("Contract address"), - value = "0x1234567890", - error = stringReference("Contract address is invalid"), - placeholder = stringReference("0x1234567890"), - onValueChange = {}, + tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( + contractAddress = TextInputFieldUM( + label = stringReference("Contract address"), + value = "0x1234567890", + error = stringReference("Contract address is invalid"), + placeholder = stringReference("0x1234567890"), + onValueChange = {}, + ), ), ), + PreviewCustomTokenFormComponent( + tokenForm = null, + ), ) } // endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt deleted file mode 100644 index 57abdb15b5..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.tangem.features.managetokens.ui - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.rows.ChainRow -import com.tangem.core.ui.components.rows.model.ChainRowUM -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent -import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent -import com.tangem.features.managetokens.entity.CurrencyNetworkUM -import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM -import com.tangem.features.managetokens.entity.SelectedNetworkUM -import com.tangem.features.managetokens.impl.R - -internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) { - val lastIndex = model.networks.lastIndex - - if (model.showTitle) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size36) - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - ) { - Text( - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing6, - ) - .padding(horizontal = TangemTheme.dimens.spacing12), - text = stringResource(R.string.add_custom_token_choose_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - } - - itemsIndexed( - items = model.networks, - key = { _, item -> item.id.value }, - ) { index, item -> - NetworkItem( - modifier = Modifier - .fillMaxWidth() - .clip( - shape = when { - !model.showTitle && index == 0 -> RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ) - index == lastIndex -> RoundedCornerShape( - bottomStart = TangemTheme.dimens.radius16, - bottomEnd = TangemTheme.dimens.radius16, - ) - else -> RectangleShape - }, - ) - .background(color = TangemTheme.colors.background.primary) - .clickable(onClick = { item.onSelectedStateChange(true) }) - .padding(horizontal = TangemTheme.dimens.spacing4), - model = item, - ) - } -} - -@Composable -private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) { - ChainRow( - modifier = modifier, - model = with(model) { - ChainRowUM( - name = name, - type = type, - icon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = model.iconResId, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, - ) - }, - action = { - AnimatedVisibility( - modifier = Modifier.size(TangemTheme.dimens.size24), - visible = model.isSelected, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - }, - ) -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview_CustomTokenNetworkSelectorContent( - @PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class) - component: CustomTokenNetworkSelectorComponent, -) { - TangemThemePreview { - LazyColumn { - component.content(this) - } - } -} - -private class CustomTokenNetworkSelectorComponentPreviewProvider : - PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - PreviewCustomTokenNetworkSelectorComponent(), - PreviewCustomTokenNetworkSelectorComponent( - params = CustomTokenNetworkSelectorComponent.Params( - userWalletId = UserWalletId(stringValue = "321"), - selectedNetwork = SelectedNetworkUM( - id = Network.ID(value = "0"), - name = "", - ), - onNetworkSelected = {}, - ), - ), - ) -} -// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt new file mode 100644 index 0000000000..f5545db44b --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -0,0 +1,255 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.ChainRow +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenSelectorComponent +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent +import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM +import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath +import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork +import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.item.DerivationPathUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription + +@Composable +internal fun CustomTokenSelectorContent(model: CustomTokenSelectorUM, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val lastIndex = model.items.lastIndex + + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), + ) { + item { + Header(model.header) + } + + itemsIndexed( + items = model.items, + key = { _, item -> item.id }, + ) { index, item -> + val itemModifier = Modifier + .fillMaxWidth() + .clip( + shape = when { + model.header !is CustomTokenSelectorUM.HeaderUM.Description && index == 0 -> { + RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ) + } + index == lastIndex -> { + RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ) + } + else -> { + RectangleShape + } + }, + ) + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = { item.onSelectedStateChange(true) }) + .padding(horizontal = TangemTheme.dimens.spacing4) + + when (item) { + is CurrencyNetworkUM -> { + NetworkItem( + modifier = itemModifier, + model = item, + ) + } + is DerivationPathUM -> { + DerivationPathItem( + modifier = itemModifier, + model = item, + ) + } + } + } + } +} + +@Composable +private fun Header(header: CustomTokenSelectorUM.HeaderUM, modifier: Modifier = Modifier) { + when (header) { + is CustomTokenSelectorUM.HeaderUM.CustomDerivationButton -> { + CustomDerivationButton( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing16), + onClick = header.onClick, + ) + } + is CustomTokenSelectorUM.HeaderUM.Description -> { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddCustomTokenDescription() + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size36) + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.bottomSheet, + ), + ) { + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing12), + text = stringResource(R.string.add_custom_token_choose_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + is CustomTokenSelectorUM.HeaderUM.None -> { + Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing16)) + } + } +} + +@Composable +private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) { + ChainRow( + modifier = modifier, + model = with(model) { + ChainRowUM( + name = name, + type = type, + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = model.iconResId, + isGrayscale = !model.isSelected, + showCustomBadge = false, + ), + showCustom = false, + ) + }, + action = { + AnimatedVisibility( + modifier = Modifier.size(TangemTheme.dimens.size24), + visible = model.isSelected, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + }, + ) +} + +@Composable +private fun CustomDerivationButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + // TODO: Implement in [REDACTED_JIRA] + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size56) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = onClick) + .padding(all = TangemTheme.dimens.spacing12), + contentAlignment = Alignment.CenterStart, + ) { + Text( + text = "Custom", + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun DerivationPathItem(model: DerivationPathUM, modifier: Modifier = Modifier) { + // TODO: Implement in [REDACTED_JIRA] + Box( + modifier = modifier + .heightIn(TangemTheme.dimens.size56) + .padding(all = TangemTheme.dimens.spacing12), + contentAlignment = Alignment.CenterStart, + ) { + Text( + text = "${model.value} ${if (model.isSelected) "- selected" else ""}", + color = TangemTheme.colors.text.primary1, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CustomTokenNetworkSelectorContent( + @PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class) + component: CustomTokenSelectorComponent, +) { + TangemThemePreview { + component.Content(modifier = Modifier) + } +} + +private class CustomTokenNetworkSelectorComponentPreviewProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewCustomTokenSelectorComponent(), + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = stringReference(""), + derivationPath = "m/44'/0'/0'/0/0", + ), + onNetworkSelected = {}, + ), + ), + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.DerivationPathSelector( + userWalletId = UserWalletId(stringValue = "321"), + selectedDerivationPath = SelectedDerivationPath( + value = "m/44'/0'/0'/0/0", + name = stringReference(""), + ), + onDerivationPathSelected = {}, + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 9f9584fac9..547963c3b0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -1,15 +1,14 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration -import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon @@ -20,6 +19,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -33,36 +38,51 @@ import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent -import com.tangem.features.managetokens.entity.CurrencyItemUM -import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM -import com.tangem.features.managetokens.entity.ManageTokensTopBarUM -import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM +import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R import kotlinx.collections.immutable.ImmutableList private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f +private const val LOAD_ITEMS_BUFFER = 10 @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { - BackHandler(onBack = state.popBack) + val keyboardController = LocalSoftwareKeyboardController.current + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + keyboardController?.hide() + + return super.onPreScroll(available, source) + } + } + } Scaffold( - modifier = modifier, + modifier = modifier.nestedScroll(nestedScrollConnection), containerColor = TangemTheme.colors.background.primary, + contentWindowInsets = WindowInsetsZero, topBar = { ManageTokensTopBar( modifier = Modifier.statusBarsPadding(), topBar = state.topBar, + search = state.search, ) }, content = { innerPadding -> @@ -70,10 +90,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi modifier = Modifier .padding(innerPadding) .fillMaxSize(), - search = state.search, - items = state.items, - isLoading = state.isLoading, - hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges, + state = state, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -81,10 +98,11 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi if (state is ManageTokensUM.ManageContent) { SaveChangesButton( modifier = Modifier + .navigationBarsPadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isVisible = state.hasChanges, - onClick = state.onSaveClick, + onClick = state.saveChanges, ) } }, @@ -92,16 +110,26 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi } @Composable -private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) { - TangemTopAppBar( - modifier = modifier, - title = topBar.title.resolveReference(), - startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), - endButton = when (topBar) { - is ManageTokensTopBarUM.ManageContent -> topBar.endButton - is ManageTokensTopBarUM.ReadContent -> null - }, - ) +private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + TangemTopAppBar( + title = topBar.title.resolveReference(), + startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), + endButton = when (topBar) { + is ManageTokensTopBarUM.ManageContent -> topBar.endButton + is ManageTokensTopBarUM.ReadContent -> null + }, + ) + SearchBar( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing16), + state = search, + ) + } } @Composable @@ -122,80 +150,58 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: } @Composable -private fun LoadingContent() { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.primary), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = TangemTheme.colors.icon.accent) - } -} - -@Composable -private fun Content( - search: SearchBarUM, - items: ImmutableList, - isLoading: Boolean, - hasChanges: Boolean, - modifier: Modifier = Modifier, -) { +private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), - items = items, - search = search, + items = state.items, + showLoadingItem = state.isNextBatchLoading, + onLoadMore = state.loadMore, + isEditable = state is ManageTokensUM.ManageContent, ) - AnimatedVisibility( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(), - visible = hasChanges, - label = "bottom_fade_visibility", - ) { - BottomFade() - } + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } - Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") { - if (it) { - LoadingContent() + Crossfade(targetState = state.isInitialBatchLoading, label = "ManageTokensLoadingContent") { isVisible -> + if (isVisible) { + ProgressIndicator( + modifier = Modifier.fillMaxSize(), + ) } } } -@OptIn(ExperimentalFoundationApi::class) @Composable -private fun Currencies(items: ImmutableList, search: SearchBarUM, modifier: Modifier = Modifier) { +private fun Currencies( + items: ImmutableList, + showLoadingItem: Boolean, + isEditable: Boolean, + onLoadMore: () -> Boolean, + modifier: Modifier = Modifier, +) { + val bottomBarHeight = with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + val listState = rememberLazyListState() + LazyColumn( modifier = modifier, + state = listState, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing76 + bottomBarHeight, + ), ) { - stickyHeader(key = "search") { - Column( - modifier = Modifier - .background(TangemTheme.colors.background.primary) - .padding( - top = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) { - SearchBar(state = search) - } - } - items( items = items, - key = CurrencyItemUM::id, + key = { it.id.value }, ) { item -> when (item) { is CurrencyItemUM.Basic -> { BasicCurrencyItem( modifier = Modifier.fillMaxWidth(), item = item, + isEditable = isEditable, ) } is CurrencyItemUM.Custom -> { @@ -206,6 +212,32 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM } } } + + if (showLoadingItem) { + item(key = "loading_item") { + ProgressIndicator( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } + } + } + + InfiniteListHandler( + listState = listState, + buffer = LOAD_ITEMS_BUFFER, + onLoadMore = onLoadMore, + ) +} + +@Composable +private fun ProgressIndicator(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background(color = TangemTheme.colors.background.primary), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = TangemTheme.colors.icon.informative) } } @@ -213,7 +245,14 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { ChainRow( modifier = modifier, - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = true, + ) + }, action = { SecondarySmallButton( config = SmallButtonConfig( @@ -226,13 +265,20 @@ private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = } @Composable -private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) { +private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, modifier: Modifier = Modifier) { val isExpanded = item.networks is NetworksUM.Expanded Column(modifier = modifier) { ChainRow( modifier = Modifier.clickable(onClick = item.onExpandClick), - model = item.model, + model = with(item) { + ChainRowUM( + name = name, + type = symbol, + icon = icon, + showCustom = false, + ) + }, action = { val rotation by animateFloatAsState( targetValue = if (isExpanded) { @@ -260,13 +306,19 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M end = TangemTheme.dimens.spacing8, ), networks = item.networks, - currencyId = item.id, + currencyId = item.id.value, + isEditable = isEditable, ) } } @Composable -private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { +private fun NetworksList( + networks: NetworksUM, + currencyId: String, + isEditable: Boolean, + modifier: Modifier = Modifier, +) { AnimatedVisibility( modifier = modifier, visible = networks is NetworksUM.Expanded, @@ -286,6 +338,7 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing8), model = with(network) { BlockchainRowUM( name = name, @@ -296,10 +349,12 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod ) }, action = { - TangemSwitch( - checked = network.isSelected, - onCheckedChange = network.onSelectedStateChange, - ) + if (isEditable) { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = network.onSelectedStateChange, + ) + } }, ) }, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt new file mode 100644 index 0000000000..3b95cd9e8c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/component/AddCustomTokenDescription.kt @@ -0,0 +1,21 @@ +package com.tangem.features.managetokens.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun AddCustomTokenDescription(modifier: Modifier = Modifier) { + Text( + modifier = modifier.fillMaxWidth(fraction = 0.7f), + text = stringResource(id = R.string.custom_token_subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt new file mode 100644 index 0000000000..848d654e08 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HasLinkedTokensWarning.kt @@ -0,0 +1,30 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HasLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_unable_hide_alert_title, + currency.name, + ), + message = stringResource( + R.string.token_details_unable_hide_alert_message, + currency.name, + currency.symbol, + network.name, + ), + confirmButton = DialogButtonUM( + title = stringResource(R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt new file mode 100644 index 0000000000..46b9cc6a39 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/dialog/HideTokenWarning.kt @@ -0,0 +1,29 @@ +package com.tangem.features.managetokens.ui.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun HideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit, onDismiss: () -> Unit) { + BasicDialog( + title = stringResource( + R.string.token_details_hide_alert_title, + currency.name, + ), + message = stringResource(R.string.token_details_hide_alert_message), + confirmButton = DialogButtonUM( + title = stringResource(R.string.token_details_hide_alert_hide), + warning = true, + onClick = onConfirm, + ), + dismissButton = DialogButtonUM( + title = stringResource(R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt new file mode 100644 index 0000000000..3e2705f9e6 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ChangedCurrenciesManager.kt @@ -0,0 +1,59 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal typealias ChangedCurrencies = Map> + +internal class ChangedCurrenciesManager { + + val currenciesToAdd: MutableStateFlow = MutableStateFlow(emptyMap()) + val currenciesToRemove: MutableStateFlow = MutableStateFlow(emptyMap()) + + fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd) + } + + fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove) + } + + fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean { + return networkId in currenciesToAdd.value[currencyId].orEmpty() || + networkId in currenciesToRemove.value[currencyId].orEmpty() + } + + private fun updateChangedItems( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + removeFromIfPresent: MutableStateFlow, + addToIfNotPresent: MutableStateFlow, + ) { + val present = removeFromIfPresent.value[currencyId].orEmpty() + + if (networkId in present) { + removeFromIfPresent.update { items -> + items.toMutableMap().apply { + val ids = present - networkId + + if (ids.isEmpty()) { + remove(currencyId) + } else { + set(currencyId, ids) + } + } + } + } else { + addToIfNotPresent.update { items -> + val alreadyAdded = items[currencyId] ?: emptySet() + if (networkId in alreadyAdded) { + return@update items + } + + items + (currencyId to alreadyAdded + networkId) + } + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt new file mode 100644 index 0000000000..62ab953402 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -0,0 +1,209 @@ +package com.tangem.features.managetokens.utils.list + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.CheckHasLinkedTokensUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.impl.R +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchListState +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class ManageTokensListManager @Inject constructor( + private val getManagedTokensUseCase: GetManagedTokensUseCase, + private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, +) : ManageTokensUiActions { + + private lateinit var scope: CoroutineScope + + private val jobHolder = JobHolder() + private val actionsFlow: MutableSharedFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private val state: MutableStateFlow = MutableStateFlow(ManageTokensListState()) + + private val changedCurrenciesManager = ChangedCurrenciesManager() + private val uiManager = ManageTokensUiManager( + state = state, + messageSender = messageSender, + dispatchers = dispatchers, + actions = this, + scopeProvider = Provider { scope }, + ) + + val currenciesToAdd: StateFlow = changedCurrenciesManager.currenciesToAdd + val currenciesToRemove: StateFlow = changedCurrenciesManager.currenciesToRemove + + @OptIn(ExperimentalCoroutinesApi::class) + val paginationStatus: Flow> = state + .mapLatest { it.status } + .distinctUntilChanged() + val uiItems: Flow> = uiManager.items + + suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope { + scope = this + + val batchFlow = getManagedTokensUseCase( + context = ManageTokensListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = this, + ), + ) + + batchFlow.state + .onEach { state -> updateState(state, userWalletId) } + .flowOn(dispatchers.default) + .launchIn(scope = this) + .saveIn(jobHolder) + + // Initial load + reload(userWalletId) + } + + suspend fun reload(userWalletId: UserWalletId?) { + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig(userWalletId, searchText = null), + ), + ) + } + + suspend fun loadMore(userWalletId: UserWalletId?, query: String) { + actionsFlow.emit( + BatchAction.LoadMore( + requestParams = ManageTokensListConfig(userWalletId, query), + ), + ) + } + + suspend fun search(userWalletId: UserWalletId?, query: String) { + state.value = ManageTokensListState() + actionsFlow.emit( + BatchAction.Reload( + requestParams = ManageTokensListConfig( + userWalletId = userWalletId, + searchText = query, + ), + ), + ) + } + + private fun updateState( + batchListState: BatchListState>, + userWalletId: UserWalletId?, + ) { + state.update { state -> + state.copy( + status = batchListState.status, + ) + } + + state.update { state -> + val newBatches = batchListState.data + val currentBatches = state.currencyBatches + + // Distinct until changed + if (newBatches.size == currentBatches.size && + newBatches.map { it.key } == currentBatches.map { it.key } && + newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } + ) { + return + } + + val canEditItems = userWalletId != null + state.copy( + userWalletId = userWalletId, + currencyBatches = newBatches, + uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), + canEditItems = canEditItems, + ) + } + } + + override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + changedCurrenciesManager.addCurrency(currencyId, networkId) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true) + } + + override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) { + changedCurrenciesManager.removeCurrency(currencyId, networkId) + + sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false) + } + + override fun checkNeedToShowRemoveNetworkWarning( + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + ): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId) + + private fun sendSelectCurrencyAction( + batchKey: Int, + currencyId: ManagedCryptoCurrency.ID, + networkId: Network.ID, + isSelected: Boolean, + ) { + val request = ManageTokensUpdateAction.AddCurrency( + currencyId = currencyId, + networkId = networkId, + isSelected = isSelected, + ) + val action = BatchAction.UpdateBatches( + keys = setOf(batchKey), + async = true, + updateRequest = request, + ) + + actionsFlow.tryEmit(action) + } + + override suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean { + return checkHasLinkedTokensUseCase(userWalletId, network).getOrElse { + Timber.e( + it, + """ + Failed to check linked tokens + |- User wallet ID: $userWalletId + |- Network ID: ${network.id} + """.trimIndent(), + ) + + val message = SnackbarMessage( + message = it.localizedMessage + ?.let(::stringReference) + ?: resourceReference(R.string.common_error), + ) + messageSender.send(message) + + false + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt new file mode 100644 index 0000000000..a140122502 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListState.kt @@ -0,0 +1,45 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus + +internal typealias ManageTokensBatchAction = BatchAction + +internal data class ManageTokensListState( + val status: PaginationStatus<*> = PaginationStatus.None, + val userWalletId: UserWalletId? = null, + val uiBatches: List>> = mutableListOf(), + val currencyBatches: List>> = mutableListOf(), + val canEditItems: Boolean = true, +) { + + fun batchIndexByCurrencyId(currencyId: ManagedCryptoCurrency.ID): Int { + return currencyBatches + .indexOfFirst { batch -> batch.data.any { it.id == currencyId } } + .takeIf { it != -1 } + ?: error("Batch with currency '$currencyId' not found") + } + + fun updateUiBatchesItem( + indexToBatch: Pair>>, + indexToItem: Pair, + ): ManageTokensListState { + val updatedUiBatch = indexToBatch.second.copy( + data = indexToBatch.second.data.toMutableList().apply { + set(indexToItem.first, indexToItem.second) + }, + ) + + return copy( + uiBatches = uiBatches.toMutableList().apply { + set(indexToBatch.first, updatedUiBatch) + }, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt new file mode 100644 index 0000000000..9d73ea1e76 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -0,0 +1,16 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal interface ManageTokensUiActions { + + fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) + + fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean + + suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt new file mode 100644 index 0000000000..546761dcfb --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -0,0 +1,203 @@ +package com.tangem.features.managetokens.utils.list + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.ContentMessage +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.ui.dialog.HasLinkedTokensWarning +import com.tangem.features.managetokens.ui.dialog.HideTokenWarning +import com.tangem.features.managetokens.utils.mapper.toUiModel +import com.tangem.features.managetokens.utils.ui.toggleExpanded +import com.tangem.features.managetokens.utils.ui.update +import com.tangem.pagination.Batch +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +internal class ManageTokensUiManager( + private val state: MutableStateFlow, + private val messageSender: UiMessageSender, + private val dispatchers: CoroutineDispatcherProvider, + private val scopeProvider: Provider, + private val actions: ManageTokensUiActions, +) { + + private val scope: CoroutineScope + get() = scopeProvider() + + @OptIn(ExperimentalCoroutinesApi::class) + val items: Flow> = state + .mapLatest { state -> + state.uiBatches.asSequence() + .flatMap { it.data } + .toImmutableList() + } + .distinctUntilChanged() + + fun createOrUpdateUiBatches( + newCurrencyBatches: List>>, + canEditItems: Boolean, + ): List>> { + val currentUiBatches = state.value.uiBatches + val batches = currentUiBatches.toMutableList() + + newCurrencyBatches.forEach { (key, data) -> + val indexToUpdate = currentUiBatches.indexOfFirst { it.key == key } + + if (indexToUpdate == -1) { + val newBatch = Batch( + key = key, + data = data.map { item -> + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + }, + ) + + batches.add(newBatch) + } else { + val uiBatchToUpdate = currentUiBatches[indexToUpdate] + + if (uiBatchToUpdate.data == data) { + return@forEach + } + + val currentCurrencyBatches = state.value.currencyBatches + val currencyBatch = currentCurrencyBatches[indexToUpdate] + val updatedBatch = uiBatchToUpdate.copy( + data = data.mapIndexed { index, item -> + if (item == currencyBatch.data[index]) { + return@mapIndexed uiBatchToUpdate.data[index] + } + + val previousUiItem = uiBatchToUpdate.data.getOrNull(index) + if (previousUiItem == null || previousUiItem.id != item.id) { + item.toUiModel( + isEditable = canEditItems, + onRemoveCustomCurrencyClick = ::removeCustomCurrency, + onExpandNetworksClick = ::toggleCurrencyNetworksVisibility, + ) + } else { + previousUiItem.update(item) + } + }, + ) + + batches[indexToUpdate] = updatedBatch + } + } + + return batches + } + + private fun removeCustomCurrency(currency: ManagedCryptoCurrency.Custom) = scope.launch(dispatchers.default) { + showRemoveNetworkWarning( + currency = currency, + network = currency.network, + isCoin = currency is ManagedCryptoCurrency.Custom.Coin, + onConfirm = { + // TODO: [REDACTED_JIRA] + }, + ) + } + + private fun toggleCurrencyNetworksVisibility(currency: ManagedCryptoCurrency.Token) = scope.launch( + dispatchers.default, + ) { + state.update { batches -> + val batchIndex = batches.batchIndexByCurrencyId(currency.id) + val currencyBatch = batches.currencyBatches[batchIndex] + val currencyIndex = currencyBatch.currencyIndexById(currency.id) + + val uiBatch = batches.uiBatches[batchIndex] + val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded( + currency = currencyBatch.data[currencyIndex], + isEditable = batches.canEditItems, + onSelectCurrencyNetwork = { networkId, isSelected -> + selectNetwork(currencyBatch.key, currency, networkId, isSelected) + }, + ) + + batches.updateUiBatchesItem( + indexToBatch = batchIndex to uiBatch, + indexToItem = currencyIndex to updatedUiItem, + ) + } + } + + private fun selectNetwork( + batchKey: Int, + currency: ManagedCryptoCurrency, + source: ManagedCryptoCurrency.SourceNetwork, + isSelected: Boolean, + ) = scope.launch(dispatchers.default) { + if (currency !is ManagedCryptoCurrency.Token) return@launch + + if (isSelected) { + actions.addCurrency(batchKey, currency.id, source.id) + } else { + if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) { + showRemoveNetworkWarning( + currency = currency, + network = source.network, + isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main, + onConfirm = { + actions.removeCurrency(batchKey, currency.id, source.id) + }, + ) + } else { + actions.removeCurrency(batchKey, currency.id, source.id) + } + } + } + + private suspend fun showRemoveNetworkWarning( + currency: ManagedCryptoCurrency, + network: Network, + isCoin: Boolean, + onConfirm: () -> Unit, + ) { + val userWalletId = state.value.userWalletId + val hasLinkedTokens = if (userWalletId == null || !isCoin) { + false + } else { + actions.checkHasLinkedTokens(userWalletId, network) + } + + val message = ContentMessage { onDismiss -> + if (hasLinkedTokens) { + HasLinkedTokensWarning( + currency = currency, + network = network, + onDismiss = onDismiss, + ) + } else { + HideTokenWarning( + currency = currency, + onConfirm = { + onConfirm() + onDismiss() + }, + onDismiss = onDismiss, + ) + } + } + + messageSender.send(message) + } + + private fun Batch>.currencyIndexById(id: ManagedCryptoCurrency.ID): Int { + return data + .indexOfFirst { it.id == id } + .takeIf { it != -1 } + ?: error("Currency with currency '$id' not found in batch #$key") + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt new file mode 100644 index 0000000000..d37da299a6 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyItemMapper.kt @@ -0,0 +1,77 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.ui.getIconRes + +internal fun ManagedCryptoCurrency.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, + onRemoveCustomCurrencyClick: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = when (this) { + is ManagedCryptoCurrency.Custom -> toUiModel(onRemoveCustomCurrencyClick) + is ManagedCryptoCurrency.Token -> toUiModel(isEditable, onExpandNetworksClick) +} + +private fun ManagedCryptoCurrency.Custom.toUiModel( + onRemoveCustomCurrency: (ManagedCryptoCurrency.Custom) -> Unit, +): CurrencyItemUM = CurrencyItemUM.Custom( + id = id, + name = name, + symbol = symbol, + icon = when (this) { + is ManagedCryptoCurrency.Custom.Coin -> { + CurrencyIconState.CoinIcon( + url = iconUrl, + fallbackResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + is ManagedCryptoCurrency.Custom.Token -> { + val background = tryGetBackgroundForTokenIcon(contractAddress) + + CurrencyIconState.TokenIcon( + url = iconUrl, + fallbackBackground = background, + fallbackTint = getTintForTokenIcon(background), + topBadgeIconResId = network.id.getIconRes(isColored = true), + isGrayscale = false, + showCustomBadge = true, + ) + } + }, + onRemoveClick = { + onRemoveCustomCurrency(this) + }, +) + +private fun ManagedCryptoCurrency.Token.toUiModel( + isEditable: Boolean, + onExpandNetworksClick: (ManagedCryptoCurrency.Token) -> Unit, +): CurrencyItemUM { + val background = TangemColorPalette.Black + + return CurrencyItemUM.Basic( + id = id, + name = name, + symbol = symbol, + icon = CurrencyIconState.TokenIcon( + url = iconUrl, + topBadgeIconResId = null, + isGrayscale = if (isEditable) !isAdded else false, + showCustomBadge = false, + fallbackTint = getTintForTokenIcon(background), + fallbackBackground = background, + ), + networks = NetworksUM.Collapsed, + onExpandClick = { + onExpandNetworksClick(this) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt new file mode 100644 index 0000000000..71d9885d89 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/CurrencyNetworksMapper.kt @@ -0,0 +1,46 @@ +package com.tangem.features.managetokens.utils.mapper + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM +import com.tangem.features.managetokens.utils.ui.getIconRes +import kotlinx.collections.immutable.toImmutableList + +internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( + isExpanded: Boolean, + isItemsEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): NetworksUM { + return if (isExpanded) { + NetworksUM.Expanded( + networks = availableNetworks.map { + it.toUiModel( + isSelected = it.id in addedIn, + isEditable = isItemsEditable, + onSelectedStateChange = onSelectedStateChange, + ) + }.toImmutableList(), + ) + } else { + NetworksUM.Collapsed + } +} + +private fun SourceNetwork.toUiModel( + isSelected: Boolean, + isEditable: Boolean, + onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, +): CurrencyNetworkUM { + return CurrencyNetworkUM( + networkId = id, + name = network.name.uppercase(), + iconResId = id.getIconRes(isColored = isSelected || !isEditable), + isSelected = isSelected || !isEditable, + type = typeName, + isMainNetwork = this is SourceNetwork.Main, + onSelectedStateChange = { selected -> + onSelectedStateChange(this, selected) + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt new file mode 100644 index 0000000000..d8b0a83776 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyItemOperations.kt @@ -0,0 +1,66 @@ +package com.tangem.features.managetokens.utils.ui + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.features.managetokens.entity.item.CurrencyItemUM +import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel +import kotlinx.collections.immutable.toImmutableList + +internal fun CurrencyItemUM.toggleExpanded( + currency: ManagedCryptoCurrency, + isEditable: Boolean, + onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit, +): CurrencyItemUM { + if (currency !is ManagedCryptoCurrency.Token) return this + + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + val isExpanded = networks !is NetworksUM.Expanded + + copy( + icon = icon.copySealed( + isGrayscale = if (isEditable) !currency.isAdded && !isExpanded else false, + ), + networks = currency.toUiNetworksModel( + isExpanded = isExpanded, + isItemsEditable = isEditable, + onSelectedStateChange = onSelectCurrencyNetwork, + ), + ) + } + } +} + +internal fun CurrencyItemUM.update(currency: ManagedCryptoCurrency): CurrencyItemUM { + return when (this) { + is CurrencyItemUM.Custom -> this + is CurrencyItemUM.Basic -> { + if (currency !is ManagedCryptoCurrency.Token) { + return this + } + + copy( + icon = icon.copySealed( + isGrayscale = networks is NetworksUM.Collapsed && !currency.isAdded, + ), + networks = updateNetworks(currency), + ) + } + } +} + +private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.Token): NetworksUM = when (networks) { + is NetworksUM.Collapsed -> networks + is NetworksUM.Expanded -> networks.copy( + networks = networks.networks.map { network -> + val isSelected = network.networkId in currency.addedIn + + network.copy( + iconResId = network.networkId.getIconRes(isSelected), + isSelected = isSelected, + ) + }.toImmutableList(), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt new file mode 100644 index 0000000000..562f0db8a2 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CurrencyNetworkOperations.kt @@ -0,0 +1,21 @@ +package com.tangem.features.managetokens.utils.ui + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM + +internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM { + return copy( + iconResId = networkId.getIconRes(isSelected), + isSelected = isSelected, + ) +} + +@DrawableRes +internal fun Network.ID.getIconRes(isColored: Boolean): Int = if (isColored) { + getActiveIconRes(value) +} else { + getGreyedOutIconRes(value) +} \ No newline at end of file diff --git a/features/markets/api/build.gradle.kts b/features/markets/api/build.gradle.kts index 0248feb86d..7257245162 100644 --- a/features/markets/api/build.gradle.kts +++ b/features/markets/api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) id("kotlin-parcelize") id("configuration") } @@ -15,4 +16,10 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.markets.models) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt similarity index 54% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index a706e5850e..b7c38d44bb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -1,21 +1,23 @@ -package com.tangem.features.markets.details.api +package com.tangem.features.markets.details import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.markets.component.BottomSheetState +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.markets.entry.BottomSheetState import kotlinx.serialization.Serializable @Stable -interface MarketsTokenDetailsComponent { +interface MarketsTokenDetailsComponent : ComposableContentComponent { @Serializable data class Params( - val token: TokenMarketSerializable, + val token: TokenMarketParams, val appCurrency: AppCurrency, ) @@ -26,7 +28,5 @@ interface MarketsTokenDetailsComponent { modifier: Modifier, ) - interface Factory { - fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent - } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt similarity index 57% rename from features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt index 5c8cc47920..07aee6c5d9 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.component +package com.tangem.features.markets.entry enum class BottomSheetState { EXPANDED, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt similarity index 92% rename from features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt index 21ec5c5772..a30498b380 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.component +package com.tangem.features.markets.entry import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt new file mode 100644 index 0000000000..dbd1b4f877 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/token/block/TokenMarketBlockComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.token.block + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import kotlinx.serialization.Serializable + +@Stable +interface TokenMarketBlockComponent : ComposableContentComponent { + + @Serializable + data class Params( + val tokenId: String, + val tokenName: String, + val tokenSymbol: String, + val tokenImageUrl: String?, + ) + + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): TokenMarketBlockComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 7d8b7d0667..320f41fbc3 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -20,6 +20,8 @@ dependencies { implementation(projects.domain.markets) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) /* Compose */ implementation(deps.compose.coil) @@ -46,6 +48,8 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) + /* Common */ implementation(projects.common.ui) implementation(projects.common.uiCharts) + implementation(projects.common.routing) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt deleted file mode 100644 index a166e55e22..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.features.markets - -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.tween -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children -import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.* -import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState -import com.arkivanov.decompose.router.stack.* -import com.arkivanov.decompose.value.Value -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.component.MarketsEntryComponent -import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.api.toSerializable -import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsEntryComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - private val marketsEntryChildFactory: MarketsEntryChildFactory, -) : MarketsEntryComponent, AppComponentContext by context { - - private val stackNavigation = StackNavigation() - - val stack: Value> = childStack( - key = "main", - source = stackNavigation, - serializer = MarketsEntryChildFactory.Child.serializer(), - initialConfiguration = MarketsEntryChildFactory.Child.TokenList, - handleBackButton = true, - childFactory = { configuration, componentContext -> - marketsEntryChildFactory.createChild( - child = configuration, - appComponentContext = childByContext(componentContext), - onTokenSelected = ::marketsListTokenSelected, - onDetailsBack = ::onDetailsBack, - ) - }, - ) - - @Suppress("LongMethod") - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - val primary = TangemTheme.colors.background.primary - val secondary = TangemTheme.colors.background.secondary - val backgroundColor = remember { Animatable(primary) } - val stackState = stack.subscribeAsState() - - LocalMainBottomSheetColor.current.value = backgroundColor.value - - Children( - stack = stackState.value, - animation = stackAnimation(slide()), - ) { - when (it.configuration) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - (it.instance as MarketsTokenDetailsComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } - MarketsEntryChildFactory.Child.TokenList -> { - (it.instance as MarketsTokenListComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } - } - } - - // order of LaunchedEffects is important here - - val activeChild = stackState.value.active.configuration - - LaunchedEffect(activeChild) { - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.animateTo( - secondary, - animationSpec = tween(durationMillis = 500), - ) - } - MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 500), - ) - } - } - } - - LaunchedEffect(bottomSheetState.value) { - if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { - when (bottomSheetState.value) { - BottomSheetState.EXPANDED -> { - backgroundColor.animateTo( - secondary, - animationSpec = tween(durationMillis = 100), - ) - } - BottomSheetState.COLLAPSED -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 100), - ) - } - } - } - } - - LaunchedEffect(primary, secondary) { - if (backgroundColor.isRunning) return@LaunchedEffect - - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(secondary) - } - MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.snapTo(primary) - } - } - } - } - - @OptIn(ExperimentalDecomposeApi::class) - private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) { - stackNavigation.pushNew( - configuration = MarketsEntryChildFactory.Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params( - token = token.toSerializable(), - appCurrency = appCurrency, - ), - ), - ) - } - - private fun onDetailsBack() { - stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList } - } - - @AssistedFactory - interface Factory : MarketsEntryComponent.Factory { - override fun create(context: AppComponentContext): DefaultMarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 402356487a..ed098bec45 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -6,24 +6,49 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent +import com.tangem.features.markets.entry.BottomSheetState +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @Stable internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: MarketsTokenDetailsComponent.Params, - @Assisted private val onBack: () -> Unit, + @Assisted params: Params, + portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { private val model: MarketsTokenDetailsModel = getOrCreateModel(params) + private val portfolioComponent = portfolioComponentFactory.create( + context = child("my_portfolio"), + params = MarketsPortfolioComponent.Params(params.token.id), + ) + + init { + componentScope.launch { + model.networksState.collectLatest { + when (it) { + is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks) + TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable() + else -> {} + } + } + } + } + @Composable override fun BottomSheetContent( bottomSheetState: State, @@ -41,23 +66,50 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( val bsState by bottomSheetState LaunchedEffect(bsState) { - model.containerBottomSheetState.value = bsState + model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED } MarketsTokenDetailsContent( - state = state, - onBackClick = onBack, - onHeaderSizeChange = onHeaderSizeChange, modifier = modifier, + backgroundColor = LocalMainBottomSheetColor.current.value, + addTopBarStatusBarPadding = false, + state = state, + onBackClick = ::navigateBack, + onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = { blockModifier -> + portfolioComponent.Content(blockModifier) + }, ) } + @Composable + override fun Content(modifier: Modifier) { + LifecycleStartEffect(Unit) { + model.isVisibleOnScreen.value = true + onStopOrDispose { + model.isVisibleOnScreen.value = false + } + } + + val state by model.state.collectAsStateWithLifecycle() + + MarketsTokenDetailsContent( + modifier = modifier, + backgroundColor = TangemTheme.colors.background.tertiary, + addTopBarStatusBarPadding = true, + state = state, + onBackClick = ::navigateBack, + onHeaderSizeChange = {}, + portfolioBlock = { blockModifier -> + portfolioComponent.Content(blockModifier) + }, + ) + } + + private fun navigateBack() = router.pop() + @AssistedFactory interface Factory : MarketsTokenDetailsComponent.Factory { - override fun create( - context: AppComponentContext, - params: MarketsTokenDetailsComponent.Params, - onBack: () -> Unit, - ): DefaultMarketsTokenDetailsComponent + override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt index eb07f7d3b3..8ef6de84d7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.markets.details.impl.di -import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent import dagger.Binds import dagger.Module diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 2a40b0c83e..d2f3aa34f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.common.ui.charts.state.* +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 @@ -17,14 +18,14 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* -import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.formatter.* import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval +import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R @@ -43,13 +44,14 @@ import javax.inject.Inject @Suppress("LargeClass", "LongParameterList") @Stable +@ComponentScoped internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, - private val getTokenQuotesUseCase: GetTokenQuotesUseCase, + private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, private val urlOpener: UrlOpener, ) : Model() { @@ -115,8 +117,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( private var lastUpdatedTimestamp: Long = DateTime.now().millis - val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) val isVisibleOnScreen = MutableStateFlow(false) + val networksState = MutableStateFlow(TokenNetworksState.Loading) val state = MutableStateFlow( MarketsTokenDetailsUM( @@ -131,11 +133,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( percent = params.token.tokenQuotes.h24Percent, useAbsoluteValue = true, ), - priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) { - PriceChangeType.DOWN - } else { - PriceChangeType.UP - }, + priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), iconUrl = params.token.imageUrl, chartState = MarketsTokenDetailsUM.ChartState( dataProducer = chartDataProducer, @@ -180,7 +178,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private fun loadQuotes() { modelScope.launch { - val result = getTokenQuotesUseCase( + val result = getTokenFullQuotesUseCase( tokenId = params.token.id, appCurrency = currentAppCurrency.value, ) @@ -209,6 +207,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( appCurrency = currentAppCurrency.value, interval = interval, tokenId = params.token.id, + preview = false, ) state.update { @@ -242,6 +241,11 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.DATA, ), + body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) { + MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) + } else { + it.body + }, ) } }.onLeft { @@ -292,6 +296,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } + val networks = result.networks + + networksState.value = if (networks.isNullOrEmpty()) { + TokenNetworksState.NoNetworksAvailable + } else { + TokenNetworksState.NetworksAvailable(networks) + } + chartDataProducer.runTransaction { updateLook { it.copy( @@ -476,7 +488,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( while (true) { delay(timeMillis) // Update quotes only when the container bottom sheet is in the expanded state - containerBottomSheetState.first { it == BottomSheetState.EXPANDED } + // and is visible on the screen isVisibleOnScreen.first { it } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index f164289fe6..c4cf1f6e3f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -48,11 +48,13 @@ internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): Bi } } +@Suppress("MagicNumber") internal fun BigDecimal?.percentChangeType(): PriceChangeType { + val scaled = this?.setScale(4, RoundingMode.HALF_UP) return when { - this == null -> PriceChangeType.NEUTRAL - this > BigDecimal.ZERO -> PriceChangeType.UP - this < BigDecimal.ZERO -> PriceChangeType.DOWN + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN else -> PriceChangeType.NEUTRAL } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt new file mode 100644 index 0000000000..dbe01ddcdc --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.details.impl.model.state + +import com.tangem.domain.markets.TokenMarketInfo + +internal sealed class TokenNetworksState { + + data object Loading : TokenNetworksState() + + data object NoNetworksAvailable : TokenNetworksState() + + data class NetworksAvailable(val networks: List) : TokenNetworksState() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index ecfc715abd..e801ed3cb0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -33,7 +34,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.disableNestedScroll @@ -45,49 +45,59 @@ import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") @Composable internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, + backgroundColor: Color, + addTopBarStatusBarPadding: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { Content( modifier = modifier, + backgroundColor = backgroundColor, state = state, onBackClick = onBackClick, onHeaderSizeChange = onHeaderSizeChange, + portfolioBlock = portfolioBlock, + addTopBarStatusBarInsets = addTopBarStatusBarPadding, ) InfoBottomSheet(config = state.infoBottomSheet) } -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") @Composable private fun Content( state: MarketsTokenDetailsUM, + backgroundColor: Color, + addTopBarStatusBarInsets: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + portfolioBlock: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, ) { - val backgroundColor = LocalMainBottomSheetColor.current.value val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } Column( modifier = modifier .drawBehind { drawRect(backgroundColor) } + .let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it } .fillMaxSize(), ) { TangemTopAppBar( - modifier = Modifier.onGloballyPositioned { - if (it.size.height > 0) { - with(density) { - onHeaderSizeChange(it.size.height.toDp()) + modifier = Modifier + .onGloballyPositioned { + if (it.size.height > 0) { + with(density) { + onHeaderSizeChange(it.size.height.toDp()) + } } - } - }, + }, title = state.tokenName, startButton = TopAppBarButtonUM.Back(onBackClick), ) @@ -122,6 +132,7 @@ private fun Content( ) { MarketTokenDetailsChart( modifier = Modifier.fillMaxWidth(), + backgroundColor = backgroundColor, state = state.chartState, ) } @@ -129,6 +140,7 @@ private fun Content( tokenMarketDetailsBody( state = state.body, + portfolioBlock = portfolioBlock, ) } } @@ -261,6 +273,7 @@ private fun Preview() { TangemThemePreview { Content( modifier = Modifier.background(TangemTheme.colors.background.tertiary), + addTopBarStatusBarInsets = false, state = MarketsTokenDetailsUM( tokenName = "Token Name", priceText = "$0.00000000324", @@ -288,6 +301,8 @@ private fun Preview() { ), onHeaderSizeChange = {}, onBackClick = {}, + backgroundColor = TangemTheme.colors.background.tertiary, + portfolioBlock = {}, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index 83bc41555e..39970e9671 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -9,17 +9,21 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color import com.tangem.common.ui.charts.MarketChart import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.rememberMarketChartState -import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData @Composable -internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { +internal fun MarketTokenDetailsChart( + state: MarketsTokenDetailsUM.ChartState, + backgroundColor: Color, + modifier: Modifier = Modifier, +) { val growingColor = TangemTheme.colors.icon.accent val fallingColor = TangemTheme.colors.icon.warning val neutralColor = TangemTheme.colors.icon.informative @@ -36,7 +40,6 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo onMarkerShown = state.onMarkerPointSelected, ) - val backgroundColor = LocalMainBottomSheetColor.current.value val bottomChartAxisHeight = getMarketChartBottomAxisHeight() Box(modifier) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 40cc2fc2f5..620e182622 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -11,16 +11,31 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData -internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) { +internal fun LazyListScope.tokenMarketDetailsBody( + state: MarketsTokenDetailsUM.Body, + portfolioBlock: @Composable (Modifier) -> Unit, +) { when (state) { MarketsTokenDetailsUM.Body.Loading -> { - loading() + item("description-loading") { + DescriptionPlaceholder(modifier = Modifier.blockPaddings()) + } + + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + + loadingInfoBlocks() } is MarketsTokenDetailsUM.Body.Content -> { if (state.description != null) { description(state.description) } + item(key = "portfolio") { + portfolioBlock(Modifier.blockPaddings()) + } + infoBlocksList(state.infoBlocks) } is MarketsTokenDetailsUM.Body.Error -> { @@ -106,11 +121,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } } -private fun LazyListScope.loading() { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - +private fun LazyListScope.loadingInfoBlocks() { item("insights-loading") { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt index 473bc3a518..6023a1c8a2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt @@ -13,7 +13,7 @@ import java.math.BigDecimal internal data class MarketsTokenDetailsUM( val tokenName: String, val priceText: String, - val iconUrl: String, + val iconUrl: String?, val dateTimeText: TextReference, val priceChangePercentText: String, val priceChangeType: PriceChangeType, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt deleted file mode 100644 index c48bcbd802..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.di - -import com.tangem.features.markets.component.MarketsEntryComponent -import com.tangem.features.markets.DefaultMarketsEntryComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface ComponentModule { - - @Binds - @Singleton - fun bindMarketsListComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt new file mode 100644 index 0000000000..1419aa7349 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -0,0 +1,94 @@ +package com.tangem.features.markets.entry.impl + +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.* +import com.arkivanov.decompose.value.Value +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.entry.BottomSheetState +import com.tangem.features.markets.entry.MarketsEntryComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.domain.markets.toSerializableParam +import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child +import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultMarketsEntryComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val marketsEntryChildFactory: MarketsEntryChildFactory, +) : MarketsEntryComponent, AppComponentContext by context { + + private val stackNavigation = StackNavigation() + + val stack: Value> = childStack( + key = "main", + source = stackNavigation, + serializer = Child.serializer(), + initialConfiguration = Child.TokenList, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + marketsEntryChildFactory.createChild( + child = configuration, + appComponentContext = childByContext( + componentContext = factoryContext, + router = createRouter(configuration), + ), + onTokenSelected = ::marketsListTokenSelected, + ) + }, + ) + + @Suppress("LongMethod") + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + EntryBottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + stackState = stack.subscribeAsState(), + modifier = modifier, + ) + } + + @OptIn(ExperimentalDecomposeApi::class) + private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) { + stackNavigation.pushNew( + configuration = Child.TokenDetails( + params = MarketsTokenDetailsComponent.Params( + token = token.toSerializableParam(), + appCurrency = appCurrency, + ), + ), + ) + } + + private fun AppComponentContext.createRouter(child: Child): Router { + return when (child) { + is Child.TokenDetails -> { + MarketTokenDetailsRouter( + contextRouter = this.router, + stackNavigation = stackNavigation, + ) + } + else -> this.router + } + } + + @AssistedFactory + interface Factory : MarketsEntryComponent.Factory { + override fun create(context: AppComponentContext): DefaultMarketsEntryComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt new file mode 100644 index 0000000000..fb1d1b1eff --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketTokenDetailsRouter.kt @@ -0,0 +1,25 @@ +package com.tangem.features.markets.entry.impl + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.popWhile +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.decompose.navigation.Router +import kotlin.reflect.KClass + +internal class MarketTokenDetailsRouter( + private val contextRouter: Router, + private val stackNavigation: StackNavigation, +) : Router by contextRouter { + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + stackNavigation.popWhile({ it != MarketsEntryChildFactory.Child.TokenList }, onComplete) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + /** Not allowed */ + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + /** Not allowed */ + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt similarity index 88% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt index 0db378d0f5..31e6f5e1e0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt @@ -1,10 +1,11 @@ -package com.tangem.features.markets +package com.tangem.features.markets.entry.impl import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent import kotlinx.serialization.Serializable import javax.inject.Inject @@ -31,14 +32,12 @@ internal class MarketsEntryChildFactory @Inject constructor( child: Child, appComponentContext: AppComponentContext, onTokenSelected: (TokenMarket, AppCurrency) -> Unit, - onDetailsBack: () -> Unit, ): Any { return when (child) { is Child.TokenDetails -> { tokenDetailsComponentFactory.create( context = appComponentContext, params = child.params, - onBack = onDetailsBack, ) } is Child.TokenList -> { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..4603041300 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.markets.entry.impl.di + +import com.tangem.features.markets.entry.MarketsEntryComponent +import com.tangem.features.markets.entry.impl.DefaultMarketsEntryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsEntryComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt new file mode 100644 index 0000000000..dccea6ad25 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -0,0 +1,128 @@ +package com.tangem.features.markets.entry.impl.ui + +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector4D +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.markets.entry.BottomSheetState +import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory +import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent + +@Composable +internal fun EntryBottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + stackState: State>, + modifier: Modifier = Modifier, +) { + val primary = TangemTheme.colors.background.primary + val secondary = TangemTheme.colors.background.secondary + val backgroundColor = remember { Animatable(primary) } + + LocalMainBottomSheetColor.current.value = backgroundColor.value + + Children( + stack = stackState.value, + animation = stackAnimation(slide()), + ) { + when (it.configuration) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + (it.instance as MarketsTokenDetailsComponent).BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + MarketsEntryChildFactory.Child.TokenList -> { + (it.instance as MarketsTokenListComponent).BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + } + } + + val activeChild = stackState.value.active.configuration + + BackgroundColorEffects( + activeChild = activeChild, + backgroundColor = backgroundColor, + bottomSheetState = bottomSheetState, + ) +} + +@Composable +private fun BackgroundColorEffects( + activeChild: MarketsEntryChildFactory.Child, + backgroundColor: Animatable, + bottomSheetState: State, +) { + val primary = TangemTheme.colors.background.primary + val secondary = TangemTheme.colors.background.secondary + + // Order of LaunchedEffects is important here + + LaunchedEffect(activeChild) { + when (activeChild) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + backgroundColor.animateTo( + secondary, + animationSpec = tween(durationMillis = 500), + ) + } + MarketsEntryChildFactory.Child.TokenList -> { + backgroundColor.animateTo( + primary, + animationSpec = tween(durationMillis = 500), + ) + } + } + } + + LaunchedEffect(bottomSheetState.value) { + if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { + when (bottomSheetState.value) { + BottomSheetState.EXPANDED -> { + backgroundColor.animateTo( + secondary, + animationSpec = tween(durationMillis = 100), + ) + } + BottomSheetState.COLLAPSED -> { + backgroundColor.animateTo( + primary, + animationSpec = tween(durationMillis = 100), + ) + } + } + } + } + + LaunchedEffect(primary, secondary) { + if (backgroundColor.isRunning) return@LaunchedEffect + + when (activeChild) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + backgroundColor.snapTo(secondary) + } + MarketsEntryChildFactory.Child.TokenList -> { + backgroundColor.snapTo(primary) + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt new file mode 100644 index 0000000000..e23fee1426 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.api + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.markets.TokenMarketInfo +import kotlinx.serialization.Serializable + +@Stable +interface MarketsPortfolioComponent : ComposableContentComponent { + + @Serializable + data class Params(val tokenId: String) + + fun setTokenNetworks(networks: List) + + fun setNoNetworksAvailable() + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt new file mode 100644 index 0000000000..be951a14a5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.markets.portfolio.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: MarketsPortfolioComponent.Params, +) : AppComponentContext by context, MarketsPortfolioComponent { + + private val model: MarketsPortfolioModel = getOrCreateModel(params) + + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) + override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() + + @Composable + override fun Content(modifier: Modifier) { + MyPortfolio( + modifier = modifier, + state = MyPortfolioUM.Loading, + ) + } + + @AssistedFactory + interface Factory : MarketsPortfolioComponent.Factory { + override fun create( + context: AppComponentContext, + params: MarketsPortfolioComponent.Params, + ): DefaultMarketsPortfolioComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..d011fbf799 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsPortfolioComponent( + factory: DefaultMarketsPortfolioComponent.Factory, + ): MarketsPortfolioComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt new file mode 100644 index 0000000000..38ea8f8688 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.portfolio.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(MarketsPortfolioModel::class) + fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt new file mode 100644 index 0000000000..ba1f20d5c9 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -0,0 +1,30 @@ +package com.tangem.features.markets.portfolio.impl.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class MarketsPortfolioModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() + + @Suppress("UnusedPrivateMember") + fun setTokenNetworks(networks: List) { + // TODO [REDACTED_TASK_KEY] + } + + fun setNoNetworksAvailable() { + // TODO [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt new file mode 100644 index 0000000000..e9b760f529 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -0,0 +1,362 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.rows.ArrowRow +import com.tangem.core.ui.components.rows.BlockchainRow +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.coroutines.delay + +@Composable +internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + addBottomInsets = false, + titleText = resourceReference(R.string.markets_add_to_portfolio_button), + ) { + Content( + modifier = Modifier.fillMaxWidth(), + state = config.content as AddToPortfolioBSContentUM, + ) + } +} + +@Composable +private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { + var continueButtonAreaHeight by remember { mutableIntStateOf(0) } + val density = LocalDensity.current + val scrollState = rememberScrollState() + + Box(modifier = modifier) { + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + UserWalletItem( + state = state.selectedWallet, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) + + SpacerH12() + + NetworkSelection( + modifier = Modifier.fillMaxWidth(), + state = state.selectNetworkUM, + ) + + SpacerH12() + + AnimatedVisibility( + visible = state.isScanCardNotificationVisible, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + ScanWalletWarning(modifier = Modifier.fillMaxWidth()) + SpacerH12() + } + + // Scroll to the bottom when the notification appears and the scroll is at the bottom + LaunchedEffect(Unit) { + if (scrollState.canScrollForward.not()) { + delay(timeMillis = 500) + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + + SpacerH(with(density) { continueButtonAreaHeight.toDp() }) + } + + AnimatedVisibility( + visible = scrollState.canScrollForward, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter), + ) { + BottomFade(Modifier.align(Alignment.BottomCenter)) + } + + ContinueButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .onGloballyPositioned { + continueButtonAreaHeight = it.size.height + }, + enabled = state.continueButtonEnabled, + onClick = state.onContinueButtonClick, + ) + } +} + +@Composable +private fun ContinueButton(enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemButton( + enabled = enabled, + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .navigationBarsPadding() + .fillMaxWidth(), + text = stringResource(R.string.common_continue), + icon = if (enabled) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, + showProgress = false, + size = TangemButtonSize.Default, + colors = TangemButtonsDefaults.primaryButtonColors, + onClick = onClick, + animateContentChange = true, + ) +} + +@Suppress("LongMethod") +@Composable +private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.markets_select_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing14), + verticalAlignment = Alignment.CenterVertically, + ) { + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = state.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + SpacerW12() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenName, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + SpacerW6() + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .alignByBaseline(), + text = state.tokenCurrencySymbol, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } + + state.networks.fastForEachIndexed { index, network -> + ArrowRow( + isLastItem = index == state.networks.lastIndex, + content = { + BlockchainRow( + modifier = Modifier.padding( + end = TangemTheme.dimens.spacing4, + ), + model = with(network) { + BlockchainRowUM( + name = name, + type = type, + iconResId = iconResId, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + }, + action = { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = { + state.onNetworkSwitchClick(network, it) + }, + ) + }, + ) + }, + ) + } + } + } +} + +@Composable +private fun ScanWalletWarning(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background( + color = TangemTheme.colors.button.disabled, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + ) { + Icon( + modifier = Modifier.requiredSize(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + Text( + text = stringResource(R.string.markets_generate_addresses_notification), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + content = content, + onDismissRequest = {}, + ), + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContent( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} + +@Composable +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun PreviewContentRtl( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview(rtl = true) { + Content( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth(), + state = content, + ) + } +} + +// For on device testing +@Composable +@Preview +private fun PreviewContentTestOnDevice( + @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, +) { + TangemThemePreview( + alwaysShowBottomSheets = false, + ) { + var isShow by remember { mutableStateOf(false) } + + var contentState by remember { + mutableStateOf(content) + } + + LaunchedEffect(Unit) { + contentState = content.copy( + onContinueButtonClick = { + contentState = contentState.copy( + isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, + ) + }, + continueButtonEnabled = true, + selectedWallet = content.selectedWallet.copy( + onClick = { + contentState = contentState.copy( + continueButtonEnabled = !contentState.continueButtonEnabled, + ) + }, + ), + ) + } + + AddToPortfolioBottomSheet( + config = TangemBottomSheetConfig( + isShow = isShow, + content = contentState, + onDismissRequest = { isShow = false }, + ), + ) + + Button( + onClick = { isShow = !isShow }, + ) { + Text(text = "Toggle") + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt new file mode 100644 index 0000000000..30cbcb453f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -0,0 +1,174 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM + +@Composable +internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + contentHorizontalPadding = 0.dp, + title = { + Text( + text = stringResource(R.string.markets_common_my_portfolio), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + if (state !is MyPortfolioUM.Tokens) return@InformationBlock + + when (state.buttonState) { + MyPortfolioUM.Tokens.AddButtonState.Loading -> { + SmallButtonShimmer( + modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + ) + } + else -> { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_add_token), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = state.onAddClick, + enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available, + ), + ) + } + } + }, + ) { + when (state) { + is MyPortfolioUM.Tokens -> TokenList(state = state) + is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state) + MyPortfolioUM.Loading -> LoadingPlaceholder() + MyPortfolioUM.Unavailable -> UnavailableContent() + } + } +} + +@Composable +private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { + Column(modifier) { + state.tokens.fastForEachIndexed { index, token -> + PortfolioItem( + state = token, + lastInList = index == state.tokens.size - 1, + ) + } + } +} + +@Composable +private fun UnavailableContent(modifier: Modifier = Modifier) { + Text( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Text( + text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.markets_add_to_portfolio_button), + onClick = state.onAddClick, + ) + } +} + +@Composable +private fun LoadingPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} + +@Preview +@Composable +private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { + TangemThemePreview(rtl = true) { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing8), + ) { + MyPortfolio(state) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt new file mode 100644 index 0000000000..035d1dbb91 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -0,0 +1,112 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState + +@Composable +internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { + Column(modifier) { + TokenItem(state = state.tokenItemState, isBalanceHidden = state.isBalanceHidden) + + PortfolioQuickActions( + modifier = Modifier.padding( + bottom = if (lastInList) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing24 + }, + ), + isVisible = state.isQuickActionsShown, + onActionClick = state.onQuickActionClick, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { + TangemThemePreview { + var quickActionsShown by remember { mutableStateOf(value = false) } + + val onItemClick = { + quickActionsShown = quickActionsShown.not() + } + + PortfolioItem( + state = tokenUM.copy( + tokenItemState = when (tokenUM.tokenItemState) { + is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + else -> tokenUM.tokenItemState + }, + isQuickActionsShown = quickActionsShown, + ), + lastInList = true, + ) + } +} + +private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + tokenUM.copy( + tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( + fiatAmountState = contentFiatAmount.copy(hasStaked = true), + ), + ), + tokenUM.copy( + tokenItemState = tokenUM.tokenItemState.copy( + fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), + cryptoAmountState = tokenUM.tokenItemState.cryptoAmountState.copy(text = DASH_SIGN), + ), + ), + tokenUM.copy(isBalanceHidden = true), + tokenUM.copy( + tokenItemState = TokenItemState.Unreachable( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemClick = {}, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.NoAddress( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState, + subtitleState = tokenUM.tokenItemState.subtitleState, + onItemLongClick = {}, + ), + ), + tokenUM.copy( + tokenItemState = TokenItemState.Loading( + id = tokenUM.tokenItemState.id, + iconState = tokenUM.tokenItemState.iconState, + titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, + subtitleState = tokenUM.tokenItemState.subtitleState, + ), + ), + ), +) { + + companion object { + val tokenUM = PreviewMyPortfolioUMProvider().sampleToken + val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as TokenFiatAmountState.Content + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt new file mode 100644 index 0000000000..1af8cde8ce --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt @@ -0,0 +1,214 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM + +@Composable +internal fun PortfolioQuickActions( + isVisible: Boolean, + onActionClick: (QuickActionUM) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = expandVertically(expandFrom = Alignment.Top), + exit = shrinkVertically(shrinkTowards = Alignment.Top), + ) { + Column(modifier = modifier) { + LineSeparator() + QuickActionItem( + state = QuickActionUM.Buy, + onClick = { onActionClick(QuickActionUM.Buy) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Exchange, + onClick = { onActionClick(QuickActionUM.Exchange) }, + ) + LineSeparator() + QuickActionItem( + state = QuickActionUM.Receive, + onClick = { onActionClick(QuickActionUM.Receive) }, + ) + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { + val lineColor = TangemTheme.colors.stroke.primary + val strokeWidth = TangemTheme.dimens.size1 + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val verticalPadding = TangemTheme.dimens.spacing2 + val startPadding = TangemTheme.dimens.spacing28 + + val height = TangemTheme.dimens.size16 + verticalPadding * 2 + + Canvas( + modifier = modifier + .animateEnterExit( + enter = expandVertically( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + ), + expandFrom = Alignment.Top, + ) + fadeIn(), + exit = shrinkVertically( + spring( + stiffness = Spring.StiffnessLow, + ), + shrinkTowards = Alignment.Top, + ) + fadeOut(), + ) + .fillMaxWidth() + .height(height), + ) { + val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() + + drawLine( + color = lineColor, + start = Offset(x, verticalPadding.toPx()), + end = Offset(x, size.height - verticalPadding.toPx()), + strokeWidth = strokeWidth.toPx(), + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.QuickActionItem( + state: QuickActionUM, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticManager = LocalHapticManager.current + + Row( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersMedium) + .clickable { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onClick() + } + .padding( + vertical = TangemTheme.dimens.spacing2, + horizontal = TangemTheme.dimens.spacing12, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), + ) { + Box( + Modifier + .animateEnterExit( + enter = scaleIn(), + exit = scaleOut(), + ) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size32), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(TangemTheme.dimens.size16), + imageVector = ImageVector.vectorResource(id = state.icon), + contentDescription = null, + tint = TangemTheme.colors.button.primary, + ) + } + Column( + modifier = Modifier + .animateEnterExit( + enter = fadeIn(), + exit = fadeOut(), + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.description.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + var isVisible by remember { mutableStateOf(true) } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(680.dp), + ) { + Button( + onClick = { isVisible = !isVisible }, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) { + Text(text = "Toggle") + } + SpacerH4() + Box( + modifier = Modifier.background(color = TangemTheme.colors.background.action), + ) { + PortfolioQuickActions( + isVisible = isVisible, + onActionClick = {}, + ) + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewRtl() { + TangemThemePreview(rtl = true) { + Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { + PortfolioQuickActions( + isVisible = true, + onActionClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt new file mode 100644 index 0000000000..cfc6c814d1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -0,0 +1,90 @@ +package com.tangem.features.markets.portfolio.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + title = { content -> + TangemBottomSheetTitle(content.title) + }, + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: TokenActionsBSContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.actions.forEachIndexed { index, action -> + val cornersToRound = when (index) { + 0 -> CornersToRound.TOP_2 + content.actions.lastIndex -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO + } + + DividerContainer( + modifier = Modifier + .clip(cornersToRound.getShape()) + .background(TangemTheme.colors.background.action) + .clickable { content.onActionClick(action) }, + showDivider = index != content.actions.lastIndex, + ) { + InputRowChecked( + text = action.text, + checked = false, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + TokenActionsBottomSheet( + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = TokenActionsBSContent( + title = "Wallet 1", + actions = TokenActionsBSContent.Action.entries.toImmutableList(), + onActionClick = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt new file mode 100644 index 0000000000..d184d3aac6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt @@ -0,0 +1,86 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM +import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { + + private val blockchainRow = BlockchainRowUM( + name = "Etherium 3", + type = "TEST", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = false, + ) + + override val values: Sequence + get() = sequenceOf( + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + blockchainRow.copy( + type = "MAIN", + isMainNetwork = true, + isSelected = true, + ), + blockchainRow, + blockchainRow, + ), + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + continueButtonEnabled = true, + onContinueButtonClick = {}, + ), + AddToPortfolioBSContentUM( + selectedWallet = UserWalletItemUM( + id = UserWalletId("1"), + name = stringReference("Wallet 1"), + information = stringReference("3 cards, 10,123$"), + imageUrl = "", + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.Arrow, + onClick = {}, + ), + selectNetworkUM = SelectNetworkUM( + tokenId = "etherium", + tokenName = "Etherium", + tokenCurrencySymbol = "ETH", + networks = persistentListOf( + blockchainRow.copy( + type = "MAIN", + isMainNetwork = true, + isSelected = true, + ), + *Array(25) { blockchainRow }, + ), + + onNetworkSwitchClick = { _, _ -> }, + iconUrl = null, + ), + isScanCardNotificationVisible = true, + continueButtonEnabled = false, + onContinueButtonClick = {}, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt new file mode 100644 index 0000000000..1f21b46f5b --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -0,0 +1,51 @@ +package com.tangem.features.markets.portfolio.impl.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, + onAddClick = {}, + ), + MyPortfolioUM.Tokens( + tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), + buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, + onAddClick = {}, + ), + MyPortfolioUM.AddFirstToken( + onAddClick = {}, + ), + MyPortfolioUM.Loading, + MyPortfolioUM.Unavailable, + ) + + val sampleToken = PortfolioTokenUM( + tokenItemState = TokenItemState.Content( + id = "", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "My wallet"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "733,71097 MATIC"), + subtitleState = TokenItemState.SubtitleState.TextContent(value = "XRP Ledger token"), + onItemClick = {}, + onItemLongClick = {}, + ), + isQuickActionsShown = false, + onQuickActionClick = {}, + isBalanceHidden = false, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt new file mode 100644 index 0000000000..6b708502ce --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class AddToPortfolioBSContentUM( + val selectedWallet: UserWalletItemUM, + val selectNetworkUM: SelectNetworkUM, + val isScanCardNotificationVisible: Boolean, + val continueButtonEnabled: Boolean, + val onContinueButtonClick: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt new file mode 100644 index 0000000000..7fa4c11a4a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class MyPortfolioUM { + + data class Tokens( + val tokens: ImmutableList, + val buttonState: AddButtonState, + val onAddClick: () -> Unit, + ) : MyPortfolioUM() { + + enum class AddButtonState { + Loading, + Available, + Unavailable, + } + } + + data class AddFirstToken( + val onAddClick: () -> Unit, + ) : MyPortfolioUM() + + data object Loading : MyPortfolioUM() + + data object Unavailable : MyPortfolioUM() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt new file mode 100644 index 0000000000..59dc297746 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.core.ui.components.token.state.TokenItemState + +internal data class PortfolioTokenUM( + val tokenItemState: TokenItemState, + val isBalanceHidden: Boolean, + val isQuickActionsShown: Boolean, + val onQuickActionClick: (QuickActionUM) -> Unit, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt new file mode 100644 index 0000000000..b079b5ce50 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -0,0 +1,30 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +@Immutable +internal enum class QuickActionUM( + val title: TextReference, + val description: TextReference, + @DrawableRes val icon: Int, +) { + Buy( + title = resourceReference(R.string.common_buy), + description = resourceReference(R.string.buy_token_description), + icon = R.drawable.ic_plus_24, + ), + Exchange( + title = resourceReference(R.string.common_exchange), + description = resourceReference(R.string.exсhange_token_description), + icon = R.drawable.ic_exchange_vertical_24, + ), + Receive( + title = resourceReference(R.string.common_receive), + description = resourceReference(R.string.receive_token_description), + icon = R.drawable.ic_arrow_down_24, + ), +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt new file mode 100644 index 0000000000..90830679ca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import kotlinx.collections.immutable.ImmutableList + +internal data class SelectNetworkUM( + val tokenId: String, + val iconUrl: String?, + val tokenName: String, + val tokenCurrencySymbol: String, + val networks: ImmutableList, + val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt new file mode 100644 index 0000000000..2252b38f70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContent.kt @@ -0,0 +1,28 @@ +package com.tangem.features.markets.portfolio.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal data class TokenActionsBSContent( + val title: String, + val actions: ImmutableList, + val onActionClick: (Action) -> Unit, +) : TangemBottomSheetConfigContent { + + @Immutable + enum class Action( + val text: TextReference, + ) { + CopyAddress(text = resourceReference(R.string.common_copy_address)), + Receive(text = resourceReference(R.string.common_receive)), + Sell(text = resourceReference(R.string.common_sell)), + Buy(text = resourceReference(R.string.common_buy)), + Send(text = resourceReference(R.string.common_send)), + Exchange(text = resourceReference(R.string.common_exchange)), + Stake(text = resourceReference(R.string.common_stake)), + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt new file mode 100644 index 0000000000..e4acc5b43c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.markets.token.block.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +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.markets.token.block.TokenMarketBlockComponent +import com.tangem.features.markets.token.block.TokenMarketBlockComponent.Params +import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel +import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultTokenMarketBlockComponent @AssistedInject constructor( + @Assisted componentContext: AppComponentContext, + @Assisted params: Params, +) : TokenMarketBlockComponent, AppComponentContext by componentContext { + + private val model: TokenMarketBlockModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + TokenMarketBlock( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : TokenMarketBlockComponent.Factory { + override fun create(appComponentContext: AppComponentContext, params: Params): DefaultTokenMarketBlockComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..0da82394c8 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.token.block.impl.di + +import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import com.tangem.features.markets.token.block.impl.DefaultTokenMarketBlockComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsPortfolioComponent( + factory: DefaultTokenMarketBlockComponent.Factory, + ): TokenMarketBlockComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ModelModule.kt new file mode 100644 index 0000000000..fdb1ba90ba --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.token.block.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(TokenMarketBlockModel::class) + fun provideTokenMarketBlockModel(model: TokenMarketBlockModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/QuotesState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/QuotesState.kt new file mode 100644 index 0000000000..218669f3d9 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/QuotesState.kt @@ -0,0 +1,8 @@ +package com.tangem.features.markets.token.block.impl.model + +import java.math.BigDecimal + +internal class QuotesState( + val currentPrice: BigDecimal, + val h24Percent: BigDecimal, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt new file mode 100644 index 0000000000..42184de78e --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -0,0 +1,142 @@ +package com.tangem.features.markets.token.block.impl.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +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.decompose.navigation.Router +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class TokenMarketBlockModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, + private val getTokenQuotesUseCase: GetTokenQuotesUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, +) : Model() { + + private val params = paramsContainer.require() + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false) + + private val currentAppCurrency = getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private var quotesState: QuotesState? = null + + val state = MutableStateFlow( + TokenMarketBlockUM( + currencySymbol = params.tokenSymbol, + currentPrice = null, + h24Percent = null, + priceChangeType = PriceChangeType.NEUTRAL, + chartData = null, + onClick = ::navigateToMarketDetails, + ), + ) + + init { + startFetching() + } + + private fun startFetching() { + modelScope.launch { + getTokenQuotesUseCase( + tokenId = params.tokenId, + interval = PriceChangeInterval.H24, + ).collect { + it.onRight { res -> + quotesState = QuotesState( + currentPrice = res.fiatRate, + h24Percent = res.priceChange, + ) + + state.value = state.value.copy( + currentPrice = BigDecimalFormatter.formatFiatPriceUncapped( + fiatAmount = res.fiatRate, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencyCode = currentAppCurrency.value.code, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ), + h24Percent = BigDecimalFormatter.formatPercent( + percent = res.priceChange, + useAbsoluteValue = true, + ), + priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), + ) + } + } + } + + modelScope.launch(dispatchers.main) { + val result = getTokenPriceChartUseCase( + tokenId = params.tokenId, + interval = PriceChangeInterval.H24, + appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY] + preview = true, + ) + + result.onRight { res -> + state.update { stateToUpdate -> + stateToUpdate.copy( + chartData = priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = res.priceY.toImmutableList(), + x = res.timeStamps.sorted().map { it.toBigDecimal() }.toImmutableList(), + ), + ), + ) + } + } + } + } + + private fun navigateToMarketDetails() { + val quotes = quotesState ?: return + + val tokenParam = TokenMarketParams( + id = params.tokenId, + name = params.tokenSymbol, + imageUrl = params.tokenImageUrl, + symbol = params.tokenSymbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = quotes.currentPrice, + h24Percent = quotes.h24Percent, + weekPercent = null, + monthPercent = null, + ), + ) + + // FIXME navigation crash [REDACTED_TASK_KEY] + router.push( + AppRoute.MarketsTokenDetails( + token = tokenParam, + appCurrency = currentAppCurrency.value, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt new file mode 100644 index 0000000000..67e5d7305e --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -0,0 +1,213 @@ +package com.tangem.features.markets.token.block.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.charts.MarketChartMini +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.details.impl.model.formatter.toChartType +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM +import kotlinx.collections.immutable.toImmutableList +import kotlin.random.Random + +@Composable +internal fun TokenMarketBlock(state: TokenMarketBlockUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + enabled = state.currentPrice != null, + onClick = state.onClick, + content = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + LeftSide( + modifier = Modifier.weight(1f), + symbol = state.currencySymbol, + priceText = state.currentPrice, + percentText = state.h24Percent, + type = state.priceChangeType, + ) + SpacerW8() + RightSide( + modifier = Modifier, + priceChangeType = state.priceChangeType, + chartRawData = state.chartData, + ) + } + }, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun LeftSide( + symbol: String, + priceText: String?, + percentText: String?, + type: PriceChangeType, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + text = stringResource(id = R.string.wallet_marketplace_block_title, symbol), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + + if (priceText != null && percentText != null) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = priceText, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + PriceChangeInPercent( + valueInPercent = percentText, + type = type, + ) + Text( + text = stringResource(id = R.string.wallet_marketprice_block_update_time), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } else { + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.6f), + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun RightSide( + priceChangeType: PriceChangeType?, + chartRawData: MarketChartRawData?, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing10), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + if (chartRawData != null && priceChangeType != null) { + MarketChartMini( + rawData = chartRawData, + type = priceChangeType.toChartType(), + modifier = Modifier + .requiredSize( + width = TangemTheme.dimens.size56, + height = TangemTheme.dimens.size24, + ), + ) + } else { + RectangleShimmer( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .requiredSize( + width = TangemTheme.dimens.size56, + height = TangemTheme.dimens.size20, + ), + ) + } + + if (priceChangeType != null) { + Icon( + modifier = Modifier.requiredSize(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } else { + RectangleShimmer( + modifier = Modifier + .requiredSize( + width = TangemTheme.dimens.size20, + height = TangemTheme.dimens.size20, + ), + ) + } + } +} + +@Preview(widthDp = 360) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 360) +@Composable +private fun Preview() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + ) + + val state = TokenMarketBlockUM( + currencySymbol = "XRP", + currentPrice = "0,5$", + h24Percent = "0,5%", + priceChangeType = PriceChangeType.UP, + chartData = data, + onClick = {}, + ) + + TangemThemePreview { + Column( + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + TokenMarketBlock( + modifier = Modifier.fillMaxWidth(), + state = state, + ) + TokenMarketBlock( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + currentPrice = "0,0000000000012356786789$", + ), + ) + TokenMarketBlock( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + currentPrice = null, + chartData = null, + ), + ) + TokenMarketBlock( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + chartData = null, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt new file mode 100644 index 0000000000..475c39e3f9 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt @@ -0,0 +1,13 @@ +package com.tangem.features.markets.token.block.impl.ui.state + +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType + +internal data class TokenMarketBlockUM( + val currencySymbol: String, + val currentPrice: String?, + val h24Percent: String?, + val priceChangeType: PriceChangeType, + val chartData: MarketChartRawData?, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt index 6843a4d610..64b0f12b74 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.entry.BottomSheetState @Stable interface MarketsTokenListComponent { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 64c1f0f751..a04a5f8b63 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import com.tangem.features.markets.tokenlist.impl.ui.MarketsList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index b2d0ca1844..e140b438b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -8,7 +8,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 6dcb360962..e7164e73ab 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -34,7 +34,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 0da161d62a..2674b90648 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -196,26 +197,4 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { state.visibleIdsChanged(visibleItems) } } -} - -@Composable -fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { - val loadMore by remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - - lastVisibleItemIndex > totalItemsNumber - buffer - } - } - - val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } - var emitted by remember(totalItemsCount) { mutableStateOf(false) } - - LaunchedEffect(loadMore) { - if (loadMore && !emitted) { - emitted = onLoadMore() - } - } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7d6614a7aa..9fe33b7bd6 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -195,7 +195,7 @@ private fun ReferralInfo( } is ReferralInfoState.Loading -> { - LoadingCondition(iconResId = R.drawable.ic_tether_28) + LoadingCondition(iconResId = R.drawable.ic_tether_24) SpacerH32() LoadingCondition(iconResId = R.drawable.ic_discount_28) } @@ -211,7 +211,7 @@ private fun Conditions(state: ReferralInfoContentState) { @Composable private fun ConditionForYou(state: ReferralInfoContentState) { - Condition(iconResId = R.drawable.ic_tether_28) { + Condition(iconResId = R.drawable.ic_tether_24) { when (state) { is ReferralInfoState.ParticipantContent -> InfoForYou( award = state.award, diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 9a3de6d8cb..14e47173a5 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index d37954025b..7d184f5227 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.converter.Converter @@ -15,7 +17,7 @@ internal class SendTransactionAlertConverter( is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( code = value.code.toString(), cause = null, - causeTextReference = value.messageReference, + causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, ) is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 53378ca7f7..dee53dccc8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference @@ -45,24 +45,24 @@ internal fun SendEventEffect(event: StateEvent, snackbarHostState: Sn @Composable internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = { onActionClick() onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), onClick = onDismiss, ) diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 15213f79d6..808810f2d7 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -61,7 +61,9 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) + implementation(projects.domain.feedback) /** Common */ implementation(projects.common.ui) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt index ad93ecd3a3..d74bc8fe32 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -6,7 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal @Immutable -sealed class FeeState { +internal sealed class FeeState { data class Content( val fee: Fee?, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt index babc93a2dd..29ce1f54f5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state -enum class InnerConfirmationStakingState { +internal enum class InnerConfirmationStakingState { ASSENT, IN_PROGRESS, COMPLETED, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt deleted file mode 100644 index c133fe00ca..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.staking.impl.R - -@Immutable -internal sealed class StakingAlertState { - - abstract val title: TextReference? - abstract val message: TextReference - open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - open val onConfirmClick: (() -> Unit)? = null - - data class GenericError( - override val title: TextReference? = TODO(), - override val onConfirmClick: () -> Unit, - ) : StakingAlertState() { - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index c338f2c047..f9da881678 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R internal sealed class StakingNotification(val config: NotificationConfig) { @@ -50,14 +47,10 @@ internal sealed class StakingNotification(val config: NotificationConfig) { ), ) { data class EarnRewards( - val subtitleResourceId: Int, - val currencyName: String, + val subtitleText: TextReference, ) : Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), - subtitle = resourceReference( - subtitleResourceId, - wrappedList(currencyName), - ), + subtitle = subtitleText, ) data class Unstake( @@ -66,7 +59,13 @@ internal sealed class StakingNotification(val config: NotificationConfig) { title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, - wrappedList(cooldownPeriodDays, cooldownPeriodDays), + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 1bf70095ff..8bb0aef58a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -3,8 +3,10 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -46,6 +48,16 @@ internal class StakingStateController @Inject constructor() { mutableUiState.update(function = titleTransformer::transform) } + fun updateEvent(event: StakingEvent?) { + mutableUiState.update { + it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) + } + } + + private fun dismissAlert() { + mutableUiState.update { it.copy(event = consumedEvent()) } + } + private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, 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 e9b4c44faa..0e08d89b1c 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 @@ -9,7 +9,8 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +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 kotlinx.collections.immutable.ImmutableList @@ -56,7 +57,6 @@ internal sealed class StakingStates { val aprRange: TextReference, val onInfoClick: (InfoType) -> Unit, val yieldBalance: InnerYieldBalanceState, - val isStakeMoreAvailable: Boolean, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt new file mode 100644 index 0000000000..b923734491 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/InfoType.kt @@ -0,0 +1,9 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +internal enum class InfoType { + ANNUAL_PERCENTAGE_RATE, + UNBONDING_PERIOD, + REWARD_CLAIMING, + WARMUP_PERIOD, + REWARD_SCHEDULE, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt index f8cc06e495..12b327a0d0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.bottomsheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -data class StakingInfoBottomSheetConfig( +internal data class StakingInfoBottomSheetConfig( val title: TextReference, val text: TextReference, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 85b64c0645..9be3086028 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,10 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -18,6 +15,8 @@ import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.toPersistentList +import org.joda.time.DateTime +import java.util.Calendar internal class YieldBalancesConverter( private val cryptoCurrencyStatusProvider: Provider, @@ -88,7 +87,7 @@ internal class YieldBalancesConverter( } val cryptoAmount = balance.amount val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - val unbondingPeriod = yield.metadata.cooldownPeriod.days + val unbonding = getUnbondingDate(balance.date) validator?.let { BalanceState( validator = validator, @@ -108,11 +107,7 @@ internal class YieldBalancesConverter( ), ), rawCurrencyId = balance.rawCurrencyId, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), + unbondingPeriod = unbonding, pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -153,4 +148,32 @@ internal class YieldBalancesConverter( BalanceType.UNKNOWN, -> false } + + private fun getUnbondingDate(date: DateTime?): TextReference { + val now = DateTime.now().millis + val nowCalendar = Calendar.getInstance() + nowCalendar.resetHours() + + val endDate = Calendar.getInstance() + endDate.timeInMillis = date?.millis ?: now + endDate.resetHours() + + val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() + return if (days > 0) { + pluralReference(R.plurals.common_in_days, days, wrappedList(days)) + } else { + resourceReference(R.string.common_today) + } + } + + private fun Calendar.resetHours() { + this[Calendar.HOUR_OF_DAY] = 0 + this[Calendar.MINUTE] = 0 + this[Calendar.SECOND] = 0 + this[Calendar.MILLISECOND] = 0 + } + + private companion object { + const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000 + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt new file mode 100644 index 0000000000..f89f99f6a0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.staking.impl.R + +@Immutable +internal sealed class StakingAlertUM : AlertUM { + + data class GenericError( + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } + + data class StakingError( + val code: String, + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference = resourceReference(R.string.common_error) + override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt similarity index 54% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt index dbc800ea01..7e7700595e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt @@ -1,6 +1,7 @@ -package com.tangem.features.staking.impl.presentation.state +package com.tangem.features.staking.impl.presentation.state.events import androidx.compose.runtime.Immutable +import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference @Immutable @@ -8,5 +9,5 @@ internal sealed class StakingEvent { data class ShowSnackBar(val text: TextReference) : StakingEvent() - data class ShowAlert(val alert: StakingAlertState) : StakingEvent() + data class ShowAlert(val alert: AlertUM) : StakingEvent() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt new file mode 100644 index 0000000000..173975c66c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.events + +import com.tangem.common.ui.alerts.SendTransactionAlertConverter +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.staking.impl.presentation.state.StakingStateController + +internal class StakingEventFactory( + private val stateController: StakingStateController, + private val popBackStack: () -> Unit, + private val onFailedTxEmailClick: (String) -> Unit, +) { + + fun createGenericErrorAlert(error: String) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.GenericError( + onConfirmClick = { onFailedTxEmailClick(error) }, + ), + ) + stateController.updateEvent(alert) + } + + fun createSendTransactionErrorAlert(error: SendTransactionError?) { + val alert = error?.let { + SendTransactionAlertConverter( + popBackStack = popBackStack, + onFailedTxEmailClick = onFailedTxEmailClick, + ).convert(error) + }?.let { + StakingEvent.ShowAlert(it) + } + stateController.updateEvent(alert) + } + + fun createStakingErrorAlert(error: StakingError) { + val alert = StakingEvent.ShowAlert( + StakingAlertUM.StakingError( + code = error.toString(), + onConfirmClick = { onFailedTxEmailClick(error.toString()) }, + ), + ) + stateController.updateEvent(alert) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 807e584ca3..f8a64a3695 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -3,6 +3,8 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R @@ -81,8 +83,10 @@ internal object ConfirmationStatePreviewData { footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", notifications = persistentListOf( StakingNotification.Warning.EarnRewards( - currencyName = "Solana", - subtitleResourceId = R.string.staking_notification_earn_rewards_text_period_day, + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), ), ), transactionDoneState = TransactionDoneState.Empty, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index f31cd5f501..66d92d8f89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -52,7 +52,6 @@ internal object InitialStakingStatePreview { ), onInfoClick = {}, yieldBalance = InnerYieldBalanceState.Empty, - isStakeMoreAvailable = true, ) val stateWithYield = defaultState.copy( 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 c8e5a5d35a..7220af1f41 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 @@ -4,11 +4,12 @@ import com.tangem.domain.staking.model.stakekit.PendingAction 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.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList -object StakingClickIntentsStub : StakingClickIntents { +@Suppress("TooManyFunctions") +internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} @@ -46,5 +47,7 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onShareClick() {} + override fun onFailedTxEmailClick(errorMessage: String) {} + override fun onActiveStake(activeStake: BalanceState) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 6b4515a6ba..3f8b253474 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -119,7 +119,6 @@ internal class SetButtonsStateTransformer : Transformer { private fun List.getSecondaryAction(): PendingAction? = getOrNull(1) private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) { - StakingStep.InitialInfo -> isStakeMoreAvailable() StakingStep.RewardsValidators -> false else -> true } @@ -170,7 +169,7 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onPrimaryClick() { when (currentStep) { StakingStep.InitialInfo -> { - val actionType = StakingActionCommonType.ENTER.takeIf { isStakeMoreAvailable() } + val actionType = StakingActionCommonType.ENTER clickIntents.onAmountValueChange("") // reset amount state clickIntents.onNextClick(actionType) } @@ -241,9 +240,4 @@ internal class SetButtonsStateTransformer : Transformer { StakingActionType.UNKNOWN -> TextReference.EMPTY null -> TextReference.EMPTY } - - private fun StakingUiState.isStakeMoreAvailable(): Boolean { - val initialState = initialInfoState as? StakingStates.InitialInfoState.Data - return initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty - } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 1e77078e6e..07ba1f9fce 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R @@ -45,8 +47,10 @@ internal class SetConfirmationStateLoadingTransformer( ) } else { StakingNotification.Warning.EarnRewards( - currencyName = yield.token.name, - subtitleResourceId = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + subtitleText = resourceReference( + id = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + formatArgs = wrappedList(yield.token.name), + ), ) }, ) 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 8d8a69e019..6402fc548e 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 @@ -18,6 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.ValidatorState +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.viewmodel.StakingClickIntents @@ -32,7 +33,6 @@ import java.math.BigDecimal internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, - private val isStakeMoreAvailable: Boolean, private val isApprovalNeeded: Boolean, private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, @@ -83,7 +83,6 @@ internal class SetInitialDataStateTransformer( infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalancesConverter.convert(Unit), - isStakeMoreAvailable = isStakeMoreAvailable, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index 06f89bdd94..f0e0ade2cc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.utils.transformer.Transformer @@ -42,12 +43,4 @@ internal class ShowInfoBottomSheetStateTransformer( ), ) } -} - -enum class InfoType { - ANNUAL_PERCENTAGE_RATE, - UNBONDING_PERIOD, - REWARD_CLAIMING, - WARMUP_PERIOD, - REWARD_SCHEDULE, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index b87f8a0251..098e959f7c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -8,8 +8,8 @@ import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, private val value: String, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index ae53eab63e..6314597d0d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val yield: Yield, + yield: Yield, ) : Transformer { private val amountRequirementStateTransformer = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index f16597ae54..59d4adfaf7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import androidx.compose.ui.text.input.ImeAction import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.extensions.resourceReference @@ -31,6 +32,7 @@ internal class AmountRequirementStateTransformer( val isRequirementError = isRequirementError(prevState, amountRequirements) return if (isRequirementError) { prevState.copy( + isPrimaryButtonEnabled = false, amountTextField = prevState.amountTextField.copy( isError = true, error = resourceReference( @@ -43,6 +45,9 @@ internal class AmountRequirementStateTransformer( ), ), ), + keyboardOptions = prevState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, + ), ), ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt new file mode 100644 index 0000000000..80ba0d9029 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt @@ -0,0 +1,79 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.events.StakingEvent + +@Composable +internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + + alertConfig?.let { + StakingAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is StakingEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is StakingEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? + + val onActionClick = state.onConfirmClick + if (onActionClick != null) { + confirmButton = DialogButtonUM( + title = state.confirmButtonText.resolveReference(), + onClick = { + onActionClick() + onDismiss() + }, + ) + dismissButton = DialogButtonUM( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) + } else { + confirmButton = DialogButtonUM( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ) + dismissButton = null + } + + BasicDialog( + message = state.message.resolveReference(), + confirmButton = confirmButton, + onDismissDialog = onDismiss, + title = state.title?.resolveReference(), + dismissButton = dismissButton, + ) +} \ No newline at end of file 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 6174dcc79c..8bd4543778 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 @@ -232,31 +232,37 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli @Composable private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { - return if (balanceType == BalanceType.UNSTAKING) { - combinedReference( - resourceReference(R.string.staking_details_unbonding_period), - annotatedReference { - appendSpace() - appendColored( - text = balance.unbondingPeriod.resolveReference(), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } else { - combinedReference( - resourceReference(R.string.app_name), - annotatedReference { - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent( - percent = balance.validator.apr.orZero(), - useAbsoluteValue = true, - ), - color = TangemTheme.colors.text.accent, - ) - }, - ) + return when (balanceType) { + BalanceType.UNSTAKING -> { + combinedReference( + resourceReference(R.string.staking_unbonding), + annotatedReference { + appendSpace() + appendColored( + text = balance.unbondingPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } + BalanceType.UNSTAKED -> { + resourceReference(R.string.staking_ready_to_withdraw) + } + else -> { + combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = balance.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index ef94fa0c1d..2bbb05b0c2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +33,8 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { + val snackbarHostState = remember { SnackbarHostState() } + BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( modifier = Modifier @@ -58,6 +61,11 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } + + StakingEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable 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/viewmodel/StakingClickIntents.kt index db14a50094..fee1e2115d 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/viewmodel/StakingClickIntents.kt @@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.stakekit.PendingAction 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.BalanceState -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -43,4 +43,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onExploreClick() fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) } \ No newline at end of file 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/viewmodel/StakingViewModel.kt index 33f5b8df76..42a33c0bfd 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/viewmodel/StakingViewModel.kt @@ -17,6 +17,11 @@ import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction @@ -24,22 +29,28 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase 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.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer @@ -52,16 +63,17 @@ import com.tangem.features.staking.impl.presentation.state.transformers.approval import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import com.tangem.utils.extensions.isSingleItem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @HiltViewModel internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, @@ -78,14 +90,21 @@ internal class StakingViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, private val submitHashUseCase: SubmitHashUseCase, - private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, + private val feedbackManager: FeedbackManager, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -114,9 +133,18 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private val stakingEventFactory: StakingEventFactory + get() = StakingEventFactory( + stateController = stateController, + popBackStack = stakingStateRouter::onBackClick, + onFailedTxEmailClick = ::onFailedTxEmailClick, + ) + private var stakingApproval: StakingApproval = StakingApproval.Empty private val allowanceTaskScheduler = SingleTaskScheduler() + private var transactionInProgress: StakingTransaction? = null + private var approvalJobHolder: JobHolder = JobHolder() init { @@ -156,6 +184,8 @@ internal class StakingViewModel @Inject constructor( val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") + val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -164,15 +194,16 @@ internal class StakingViewModel @Inject constructor( actionCommonType = value.actionType, integrationId = yield.id, amount = amountValue, - address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address"), + address = defaultAddress, validatorAddress = validatorState.chosenValidator.address, token = yield.token, passthrough = pendingAction?.passthrough, type = pendingAction?.type, ), ).getOrElse { - error(it) + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch } stakingTransaction @@ -182,11 +213,18 @@ internal class StakingViewModel @Inject constructor( networkId = cryptoCurrencyStatus.currency.network.id.value, fee = fee, transactionId = transaction.id, - ).getOrNull() ?: error("No constructed transaction") + ).getOrElse { + Timber.e(it.toString()) + stakingEventFactory.createStakingErrorAlert(it) + return@launch + } + val gasEstimate = constructedTransaction.gasEstimate + ?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null") + transactionInProgress = constructedTransaction sendStakingTransaction( transactionId = constructedTransaction.id, - gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), + gasEstimate = gasEstimate, txData = transactionData, pendingActionList = confirmationState.pendingActions, ) @@ -288,8 +326,7 @@ internal class StakingViewModel @Inject constructor( userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, ).getOrElse { - // TODO staking error - return + return stakingEventFactory.createGenericErrorAlert(it.toString()) } stateController.update( @@ -306,7 +343,7 @@ internal class StakingViewModel @Inject constructor( } override fun onInitialInfoBannerClick() { - // innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) + innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } override fun onInfoClick(infoType: InfoType) { @@ -318,7 +355,7 @@ internal class StakingViewModel @Inject constructor( } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield)) } override fun onAmountPasteTriggerDismiss() { @@ -339,7 +376,16 @@ internal class StakingViewModel @Inject constructor( stateController.update(ValidatorSelectChangeTransformer(validator)) } - override fun openRewardsValidators() = onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS) + override fun openRewardsValidators() { + val rewardsValidators = + stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data + val rewards = rewardsValidators?.rewards + if (rewards != null && rewards.isSingleItem()) { + onActiveStake(rewards.first()) + } else { + onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS) + } + } override fun onActiveStake(activeStake: BalanceState) { val actionType = if (activeStake.pendingActions.isEmpty()) { @@ -348,7 +394,7 @@ internal class StakingViewModel @Inject constructor( StakingActionCommonType.PENDING_OTHER } stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) onNextClick(actionType, activeStake.pendingActions) } @@ -399,7 +445,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString()) return@launch }, ifRight = { it }, @@ -419,7 +465,7 @@ internal class StakingViewModel @Inject constructor( fee = TransactionFee.Single(fee), ), ) - // TODO staking error + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { stateController.update(SetApprovalInProgressTransformer) @@ -481,6 +527,42 @@ internal class StakingViewModel @Inject constructor( // TODO staking [REDACTED_TASK_KEY] } + override fun onFailedTxEmailClick(errorMessage: String) { + viewModelScope.launch { + val network = cryptoCurrencyStatus.currency.network + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } + val amountState = uiState.value.amountState as? AmountState.Data + val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val feeState = confirmationState?.feeState as? FeeState.Content + + val validator = validatorState?.chosenValidator + val feeAmount = feeState?.fee?.amount + val amount = amountState?.amountTextField?.cryptoAmount + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = network.id.value, + derivationPath = network.derivationPath.value, + destinationAddress = validator?.address.orEmpty(), + tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol, + amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), + ), + ) + + val email = FeedbackEmailType.StakingProblem( + cardInfo = cardInfo, + validatorName = validator?.name, + transactionType = transactionInProgress?.type?.name, + unsignedTransaction = transactionInProgress?.unsignedTransaction, + ) + + feedbackManager.sendEmail(email) + } + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -497,7 +579,8 @@ internal class StakingViewModel @Inject constructor( userWallet = wallet }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( @@ -507,13 +590,10 @@ internal class StakingViewModel @Inject constructor( setupApprovalNeeded() - val networkId = cryptoCurrencyStatus.currency.network.id - val isStakeMoreAvailable = isStakeMoreAvailableUseCase(networkId) stateController.update( transformer = SetInitialDataStateTransformer( clickIntents = this@StakingViewModel, yield = yield, - isStakeMoreAvailable = isStakeMoreAvailable.getOrElse { false }, isApprovalNeeded = stakingApproval is StakingApproval.Needed, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, @@ -522,7 +602,8 @@ internal class StakingViewModel @Inject constructor( ) }, ifLeft = { - // TODO staking error + Timber.e(it.toString()) + stakingEventFactory.createGenericErrorAlert(it.toString()) }, ) } @@ -571,11 +652,12 @@ internal class StakingViewModel @Inject constructor( pendingActionList = pendingActionList, ), ) - // todo add error dialog + stakingEventFactory.createSendTransactionErrorAlert(error) }, ifRight = { txHash -> + transactionInProgress = null submitHash(transactionId, txHash) - updateStakeBalance() + scheduleUpdates() val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, @@ -608,14 +690,55 @@ internal class StakingViewModel @Inject constructor( } } - private fun updateStakeBalance() { - viewModelScope.launch { - stakingYieldBalanceUseCase( + private fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + }, + // we should update tx history and network for new balances + async { + updateStakeBalance() + }, + async { + updateTxHistory() + }, + async { + updateNetworkStatuses() + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses() { + updateDelayedNetworkStatusUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + delayMillis = BALANCE_UPDATE_DELAY, + refresh = true, + ) + } + + private suspend fun updateStakeBalance() { + stakingYieldBalanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWalletId, + currency = cryptoCurrencyStatus.currency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( userWalletId = userWalletId, - address = CryptoCurrencyAddress( - cryptoCurrencyStatus.currency, - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - ), + currency = cryptoCurrencyStatus.currency, refresh = true, ) } @@ -628,7 +751,8 @@ internal class StakingViewModel @Inject constructor( } private companion object { - const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" + const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 11_000L } } \ No newline at end of file diff --git a/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt b/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt deleted file mode 100644 index c9b4962de4..0000000000 --- a/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.feature.swap.api - -/** Feature toggles manager of "swap" feature */ -interface SwapFeatureToggleManager { - - val isOptimismSwapEnabled: Boolean -} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 31b1659a73..f3a0d6cbe4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -19,7 +19,6 @@ import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWit import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.express.models.response.TxDetails -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency @@ -44,7 +43,6 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo as Networ @Suppress("LongParameterList", "LargeClass") internal class DefaultSwapRepository @Inject constructor( - private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, @@ -188,29 +186,6 @@ internal class DefaultSwapRepository @Inject constructor( } } - override suspend fun getRates(currencyId: String, tokenIds: List): Map { - // workaround cause backend do not return arbitrum and optimism rates - val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) { - tokenIds.toMutableList().apply { - add(ETHEREUM_ID) - } - } else { - tokenIds - } - return withContext(coroutineDispatcher.io) { - val rates = tangemTechApi.getRates(currencyId.lowercase(), addedTokens.joinToString(",")).rates - val ethRate = rates[ETHEREUM_ID] - if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) { - rates.toMutableMap().apply { - put(OPTIMISM_ID, ethRate ?: 0.0) - put(ARBITRUM_ID, ethRate ?: 0.0) - } - } else { - rates - } - } - } - override suspend fun findBestQuote( fromContractAddress: String, fromNetwork: String, @@ -432,11 +407,4 @@ internal class DefaultSwapRepository @Inject constructor( DataError.UnknownError } } - - companion object { - // TODO("get this ids from blockchain enum later") - private const val OPTIMISM_ID = "optimistic-ethereum" - private const val ARBITRUM_ID = "arbitrum-one" - private const val ETHEREUM_ID = "ethereum" - } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 5cc2b81a41..fde540c8ab 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -3,7 +3,6 @@ package com.tangem.feature.swap.di import com.squareup.moshi.Moshi import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -28,7 +27,6 @@ internal class SwapDataModule { @Provides @Singleton internal fun provideSwapRepository( - tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, @@ -38,7 +36,6 @@ internal class SwapDataModule { @NetworkMoshi moshi: Moshi, ): SwapRepository { return DefaultSwapRepository( - tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index d5469ce433..99bc772d79 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -14,8 +14,6 @@ interface SwapRepository { /** Express getPairs request variant without providers request */ suspend fun getPairsOnly(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders - suspend fun getRates(currencyId: String, tokenIds: List): Map - suspend fun getExchangeStatus(txId: String): Either @Suppress("LongParameterList") diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt deleted file mode 100644 index 15ef8179e1..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.swap.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.feature.swap.api.SwapFeatureToggleManager -import com.tangem.feature.swap.toggles.DefaultFeatureTogglesManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.hilt.android.scopes.ActivityRetainedScoped - -@Module -@InstallIn(ActivityRetainedComponent::class) -internal object SwapFeatureTogglesManagerModule { - - @Provides - @ActivityRetainedScoped - fun provideSwapFeatureTogglesManager(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggleManager { - return DefaultFeatureTogglesManager(featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt deleted file mode 100644 index 2448fefafd..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.swap.toggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -/** Feature toggles manager implementation of "swap" feature */ -class DefaultFeatureTogglesManager( - private val featureTogglesManager: FeatureTogglesManager, -) : InnerSwapFeatureTogglesManager { - - override val isOptimismSwapEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "OPTIMISM_SWAP_FEATURE_ENABLED") -} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt deleted file mode 100644 index f1035d737a..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.feature.swap.toggles - -import com.tangem.feature.swap.api.SwapFeatureToggleManager - -/** Feature toggles manager of "swap" feature for internal logic */ -internal interface InnerSwapFeatureTogglesManager : SwapFeatureToggleManager \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c013b188e0..c88a51d613 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -363,8 +363,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -996,8 +996,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = stringReference("//TODO"), + description = stringReference("//TODO"), ), ) return uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index f3004d1c73..ea5ce81e01 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -110,7 +110,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi BasicDialog( title = state.alert.title?.resolveReference(), message = message, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), onClick = state.alert.onClick, ), diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 73f39eae8c..de5b9043b9 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) implementation(projects.core.featuretoggles) + implementation(projects.core.decompose) implementation(projects.libs.crypto) @@ -77,6 +78,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) implementation(projects.domain.staking) + implementation(projects.domain.markets.models) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) @@ -87,5 +89,6 @@ dependencies { implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) implementation(projects.features.staking.api) + implementation(projects.features.markets.api) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 25e7d71ad8..9385875fb1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -1,17 +1,29 @@ package com.tangem.feature.tokendetails.presentation +import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.defaultComponentContext +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.routing.utils.asRouter +import com.tangem.core.decompose.context.DefaultAppComponentContext +import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel +import com.tangem.features.markets.MarketsFeatureToggles +import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -24,17 +36,74 @@ internal class TokenDetailsFragment : ComposeFragment() { @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter + @Inject + internal lateinit var marketsFeatureToggles: MarketsFeatureToggles + + @Inject + internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider + + @Inject + internal lateinit var componentBuilder: DecomposeComponent.Builder + + @Inject + internal lateinit var tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory + + @Inject + internal lateinit var appRouter: AppRouter + + private var tokenMarketBlockComponent: TokenMarketBlockComponent? = null + private val internalTokenDetailsRouter: InnerTokenDetailsRouter get() = requireNotNull(tokenDetailsRouter as? InnerTokenDetailsRouter) { "internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter" } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (marketsFeatureToggles.isFeatureEnabled) { + val cryptoCurrency: CryptoCurrency = arguments + ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("Token Details screen can't open without `CryptoCurrency`") + + val param = cryptoCurrency.toParam() ?: return + + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ) + + tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create( + appComponentContext = appContext, + params = param, + ) + } + } + + private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? { + val tokenId = id.rawCurrencyId ?: return null // token price is not available + + return TokenMarketBlockComponent.Params( + tokenId = tokenId, + tokenName = name, + tokenSymbol = symbol, + tokenImageUrl = iconUrl, + ) + } + @Composable override fun ScreenContent(modifier: Modifier) { val viewModel = hiltViewModel() viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) NavigationBar3ButtonsScrim() - TokenDetailsScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value) + TokenDetailsScreen( + state = viewModel.uiState.collectAsStateWithLifecycle().value, + tokenMarketBlockComponent = tokenMarketBlockComponent, + ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index e7625b4013..9c69cd0b55 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -143,9 +143,14 @@ internal object TokenDetailsPreviewData { val stakingErrorBlock = StakingBlockUM.Error(iconState) val stakingAvailableBlock = StakingBlockUM.StakeAvailable( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList("3.27%"), + ), + subtitleText = resourceReference( + id = R.string.staking_notification_earn_rewards_text_period_day, + formatArgs = wrappedList("Solana"), + ), iconState = iconState, onStakeClicked = {}, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt index 91d76db70d..b29d476a32 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -21,9 +21,8 @@ internal sealed interface StakingBlockUM { data class StakeAvailable( val iconState: IconState, - val interestRate: String, - val periodInDays: Int, - val tokenSymbol: String, + val titleText: TextReference, + val subtitleText: TextReference, val onStakeClicked: () -> Unit, ) : StakingBlockUM } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 6aa3923cd9..0555333c41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -22,6 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R @@ -191,13 +192,19 @@ internal class TokenDetailsLoadedBalanceConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, ): StakingBlockUM.StakeAvailable { + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) return StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = stakingEntryInfo.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = stakingEntryInfo.periodInDays, - tokenSymbol = stakingEntryInfo.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 3bc3e36023..b12e12eb53 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -1,12 +1,16 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -24,14 +28,20 @@ internal class TokenStakingStateConverter( ifLeft = { StakingBlockUM.Error(iconState = iconState) }, - ifRight = { + ifRight = { stakingEntryInfo -> + val apr = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.apr, + useAbsoluteValue = true, + ) StakingBlockUM.StakeAvailable( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, + titleText = resourceReference( + id = R.string.token_details_staking_block_title, + formatArgs = wrappedList(apr), + ), + subtitleText = resourceReference( + id = stakingEntryInfo.rewardSchedule.getStringResourceId(), + formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, iconState = iconState, onStakeClicked = clickIntents::onStakeBannerClick, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt index faa0144943..fe714ff2ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import java.math.BigDecimal -fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { +internal fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { this.plus(stakingAmount) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt new file mode 100644 index 0000000000..8bb6550f79 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/RewardScheduleUtils.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.features.tokendetails.impl.R + +internal fun Yield.Metadata.RewardSchedule.getStringResourceId(): Int { + return when (this) { + Yield.Metadata.RewardSchedule.BLOCK, + Yield.Metadata.RewardSchedule.DAY, + Yield.Metadata.RewardSchedule.ERA, + Yield.Metadata.RewardSchedule.EPOCH, + -> R.string.staking_notification_earn_rewards_text_period_day + + Yield.Metadata.RewardSchedule.HOUR, + -> R.string.staking_notification_earn_rewards_text_period_hour + + Yield.Metadata.RewardSchedule.WEEK, + -> R.string.staking_notification_earn_rewards_text_period_week + + Yield.Metadata.RewardSchedule.MONTH, + -> R.string.staking_notification_earn_rewards_text_period_month + + else + -> R.string.staking_notification_earn_rewards_text_period_day + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 378b55e62d..8ced7dd13a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -52,12 +52,13 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock +import com.tangem.features.markets.token.block.TokenMarketBlockComponent // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -internal fun TokenDetailsScreen(state: TokenDetailsState) { +internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockComponent: TokenMarketBlockComponent?) { BackHandler(onBack = state.topAppBarConfig.onBackClick) val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } @@ -139,12 +140,27 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } }, ) - if (state.isMarketPriceAvailable) { - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, - ) + + when { + tokenMarketBlockComponent != null -> { + item( + key = TokenMarketBlockComponent::class.java, + contentType = TokenMarketBlockComponent::class.java, + content = { tokenMarketBlockComponent.Content(modifier = itemModifier) }, + ) + } + state.isMarketPriceAvailable -> { + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { + MarketPriceBlock( + modifier = itemModifier, + state = state.marketPriceBlockState, + ) + }, + ) + } } if (state.isStakingBlockShown) { @@ -219,7 +235,10 @@ private fun TokenDetailsScreenPreview( @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, ) { TangemThemePreview { - TokenDetailsScreen(state) + TokenDetailsScreen( + state = state, + tokenMarketBlockComponent = null, + ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt index ed54c9459b..5a4f590fe7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import androidx.compose.runtime.Composable import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig @@ -19,7 +19,7 @@ internal fun TokenDetailsDialogs(state: TokenDetailsState) { private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { BasicDialog( message = config.content.message.resolveReference(), - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = config.content.confirmButtonConfig.text.resolveReference(), warning = config.content.confirmButtonConfig.warning, onClick = config.content.confirmButtonConfig.onClick, @@ -27,7 +27,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { onDismissDialog = config.onDismissRequest, title = config.content.title?.resolveReference(), dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> - DialogButton( + DialogButtonUM( title = cancelButtonConfig.text.resolveReference(), warning = cancelButtonConfig.warning, onClick = cancelButtonConfig.onClick, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index c2d7940cbc..9d0087292d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -86,10 +88,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi SpacerW8() Column { Text( - text = stringResource( - R.string.token_details_staking_block_title, - state.interestRate, - ), + text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, ) @@ -97,11 +96,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) Text( - text = stringResource( - R.string.token_details_staking_block_subtitle, - state.tokenSymbol, - state.periodInDays, - ), + text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) @@ -161,8 +156,9 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Loading", // TODO staking - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + showProgress = true, + text = TextReference.EMPTY.resolveReference(), + onClick = { /* no-op */ }, ) } } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 14885a740b..e472817d13 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - API */ implementation(projects.features.walletSettings.api) + implementation(projects.features.manageTokens.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 4dba365556..4c4d0e3f25 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -8,6 +8,7 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.features.managetokens.ManageTokensToggles internal class PreviewWalletSettingsComponent : WalletSettingsComponent { @@ -15,6 +16,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { popBack = {}, items = ItemsBuilder( router = DummyRouter(), + manageTokensToggles = object : ManageTokensToggles { + override val isFeatureEnabled: Boolean = true + }, ).buildItems( userWalletId = UserWalletId("011"), userWalletName = "My Wallet", diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index a9e5b42932..fcb721457e 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -13,7 +13,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage @@ -93,7 +93,7 @@ internal class WalletSettingsModel @Inject constructor( BasicDialog( message = stringResource(R.string.user_wallet_list_delete_prompt), onDismissDialog = onDismiss, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(R.string.common_delete), warning = true, onClick = { @@ -101,7 +101,7 @@ internal class WalletSettingsModel @Inject constructor( onDismiss() }, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(R.string.common_cancel), onClick = onDismiss, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 5ff04fe2e7..8cbce3fc72 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent @@ -21,18 +21,18 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { TextInputDialog( title = stringResource(id = R.string.user_wallet_list_rename_popup_title), fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = stringResource(id = R.string.common_ok), enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ), onDismissDialog = onDismiss, onValueChange = model.updateValue, - textFieldParams = AdditionalTextInputDialogParams( + textFieldParams = AdditionalTextInputDialogUM( label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), ), ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 941f1c53f2..4a7d294e77 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.managetokens.ManageTokensToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -17,6 +18,7 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, + private val manageTokensToggles: ManageTokensToggles, ) { @Suppress("LongParameterList") @@ -50,6 +52,14 @@ internal class ItemsBuilder @Inject constructor( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { + if (manageTokensToggles.isFeatureEnabled) { + BlockUM( + text = resourceReference(R.string.add_tokens_title), + iconRes = R.drawable.ic_tether_24, + onClick = { router.push(AppRoute.ManageTokens(userWalletId)) }, + ).let(::add) + } + if (isLinkMoreCardsAvailable) { BlockUM( text = resourceReference(R.string.details_row_title_create_backup), diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 9d6ff75448..4cdbb44b40 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -92,4 +92,8 @@ dependencies { implementation(projects.features.details.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.markets.api) + + /** Test libraries */ + implementation(deps.test.junit) + implementation(deps.test.truth) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 868de285aa..9d9206c566 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.markets.MarketsFeatureToggles -import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.AndroidEntryPoint diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 0ad7e674a7..16cf6e8e9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -2,13 +2,12 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -66,122 +65,22 @@ internal object WalletPreviewData { ) } - val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_polygon_22, - isGrayscale = false, - showCustomBadge = false, - ) - - private val tokenIconState - get() = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - showCustomBadge = false, - ) - - private val customTokenIconState - get() = CurrencyIconState.CustomTokenIcon( - tint = TangemColorPalette.Black, - background = TangemColorPalette.Meadow, - topBadgeIconResId = R.drawable.img_polygon_22, - isGrayscale = false, - ) - - val tokenItemVisibleState by lazy { - TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = coinIconState, - titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true), - cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), - cryptoPriceState = TokenItemState.CryptoPriceState.Unknown, - onItemClick = {}, - onItemLongClick = {}, - ) - } - - val testnetTokenItemVisibleState by lazy { - tokenItemVisibleState.copy( - titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"), - iconState = tokenIconState.copy(isGrayscale = true), - ) - } - - val tokenItemHiddenState by lazy { - TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = tokenIconState, - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false), - cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( - price = "312 USD", - priceChangePercent = "2.0%", - type = PriceChangeType.UP, - ), - onItemClick = {}, - onItemLongClick = {}, - ) - } - - val tokenItemDragState by lazy { + private val tokenItemDragState by lazy { TokenItemState.Draggable( id = UUID.randomUUID().toString(), - iconState = tokenIconState, + iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + showCustomBadge = false, + ), titleState = TokenItemState.TitleState.Content(text = "Polygon"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"), ) } - val tokenItemUnreachableState by lazy { - TokenItemState.Unreachable( - id = UUID.randomUUID().toString(), - iconState = tokenIconState, - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - onItemClick = {}, - onItemLongClick = {}, - ) - } - - val tokenItemNoAddressState by lazy { - TokenItemState.NoAddress( - id = UUID.randomUUID().toString(), - iconState = tokenIconState, - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - onItemLongClick = {}, - ) - } - - val customTokenItemVisibleState by lazy { - tokenItemVisibleState.copy( - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - iconState = customTokenIconState.copy( - tint = TangemColorPalette.White, - background = TangemColorPalette.Black, - ), - ) - } - - val customTestnetTokenItemVisibleState by lazy { - tokenItemVisibleState.copy( - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - iconState = customTokenIconState.copy(isGrayscale = true), - ) - } - - val loadingTokenItemState by lazy { - TokenItemState.Loading( - id = "Loading#1", - iconState = customTokenIconState.copy(isGrayscale = true), - titleState = TokenItemState.TitleState.Content(text = "Polygon"), - ) - } - private const val networksSize = 10 private const val tokensSize = 3 private val draggableItems by lazy { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt index 77f6e6160b..aca25673e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt @@ -56,30 +56,46 @@ internal fun DraggableNetworkGroupItem( ) } +// TODO: [REDACTED_JIRA] @Composable private fun InternalNetworkGroupItem( networkName: String, modifier: Modifier = Modifier, - endIcon: @Composable RowScope.() -> Unit = {}, + endIcon: @Composable (RowScope.() -> Unit)? = null, ) { - Column(modifier = modifier) { - Row( - modifier = Modifier - .background(TangemTheme.colors.background.primary) - .padding(horizontal = TangemTheme.dimens.spacing12) - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size40), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(id = R.string.wallet_network_group_title, networkName), - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing8), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - endIcon() - } + val minHeight = if (endIcon == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40 + val padding = if (endIcon == null) { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ) + } else { + PaddingValues( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing11, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing5, + ) + } + + Row( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth() + .heightIn(min = minHeight) + .padding(paddingValues = padding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = R.string.wallet_network_group_title, networkName), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + if (endIcon != null) endIcon() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 635afd0fc9..63585b4bbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference @@ -9,7 +10,6 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.model.* import kotlinx.collections.immutable.persistentListOf @@ -20,7 +20,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "34 496,75 \$", priceChangePercent = "0,43 %", type = PriceChangeType.DOWN, @@ -46,7 +46,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Ethereum"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "1,856660295 ETH"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "1 799,41 \$", priceChangePercent = "5,16 %", type = PriceChangeType.UP, @@ -68,7 +68,7 @@ internal object WalletScreenPreviewData { titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "6 200 220,00 SHIB"), - cryptoPriceState = TokenItemState.CryptoPriceState.Content( + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( price = "0.01 \$", priceChangePercent = "1,34 %", type = PriceChangeType.DOWN, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 6c3aea984f..437fd6b480 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme @@ -40,7 +41,6 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem -import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index f7c411a377..37387e16c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.model import androidx.compose.runtime.Immutable -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 93f8267b5e..73eaaeb43b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index a70acf53cd..421f33f996 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -25,7 +25,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.entry.MarketsEntryComponent import kotlin.properties.Delegates /** Default implementation of wallet feature router */ @@ -139,8 +139,8 @@ internal class DefaultWalletRouter( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openManageTokensScreen() { - router.push(AppRoute.ManageTokens(readOnlyContent = false)) + override fun openManageTokensScreen(userWalletId: UserWalletId) { + router.push(AppRoute.ManageTokens(userWalletId = userWalletId)) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index ee4229c970..40ebf62fed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter /** @@ -54,7 +54,7 @@ internal interface InnerWalletRouter : WalletRouter { fun isWalletLastScreen(): Boolean /** Open manage tokens screen */ - fun openManageTokensScreen() + fun openManageTokensScreen(userWalletId: UserWalletId) /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt new file mode 100644 index 0000000000..44d5b5b268 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/NoteImage.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.feature.wallet.impl.R + +/** + * Note image model + * + * @property blockchain blockchain + * @property imageResId image res id + * +[REDACTED_AUTHOR] + */ +internal enum class NoteImage( + val blockchain: Blockchain, + @DrawableRes val imageResId: Int, +) { + + Bitcoin(blockchain = Blockchain.Bitcoin, imageResId = R.drawable.ill_note_btc_120_106), + + Ethereum(blockchain = Blockchain.Ethereum, imageResId = R.drawable.ill_note_ethereum_120_106), + + Binance(blockchain = Blockchain.BSC, imageResId = R.drawable.ill_note_binance_120_106), + + Dogecoin(blockchain = Blockchain.Dogecoin, imageResId = R.drawable.ill_note_doge_120_106), + + Cardano(blockchain = Blockchain.Cardano, imageResId = R.drawable.ill_note_cardano_120_106), + + XRP(blockchain = Blockchain.XRP, imageResId = R.drawable.ill_note_xrp_120_106), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt deleted file mode 100644 index a78ded4bf0..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.models.UserWallet - -fun UserWallet.getCardsCount(): Int? { - return if (isMultiCurrency) { - when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.NoBackup, - is CardDTO.BackupStatus.CardLinked, - -> 1 - null -> 1 // Multi-currency wallet without backup function. Example, 4.12 - } - } else { - null - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt new file mode 100644 index 0000000000..82d07f7000 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import androidx.annotation.DrawableRes +import com.tangem.feature.wallet.impl.R + +/** + * Wallet2 cobrand image. + * To integrate a new cobrand, just implement new enum object and that all. + * + * @property cards2ResId image resource id for wallet set with 2 cards + * @property cards3ResId image resource id for wallet set with 3 cards + * @property batchIds set of unique batch ids for this cobrand + * +[REDACTED_AUTHOR] + */ +internal enum class Wallet2CobrandImage( + @DrawableRes val cards2ResId: Int, + @DrawableRes val cards3ResId: Int, + val batchIds: Set, +) { + + Avrora( + cards2ResId = R.drawable.ill_avrora_card2_120_106, + cards3ResId = R.drawable.ill_avrora_card3_120_106, + batchIds = setOf("AF18"), + ), + + BabyDoge( + cards2ResId = R.drawable.ill_baby_doge_card2_120_106, + cards3ResId = R.drawable.ill_baby_doge_card3_120_106, + batchIds = setOf("AF51"), + ), + + Bad( + cards2ResId = R.drawable.ill_bad_card2_120_106, + cards3ResId = R.drawable.ill_bad_card3_120_106, + batchIds = setOf("AF09"), + ), + + BitcoinPizzaDay( + cards2ResId = R.drawable.ill_pizza_day_card2_120_106, + cards3ResId = R.drawable.ill_pizza_day_card3_120_106, + batchIds = setOf("AF33"), + ), + + CoinMetrica( + cards2ResId = R.drawable.ill_coin_metrica_card2_120_106, + cards3ResId = R.drawable.ill_coin_metrica_card3_120_106, + batchIds = setOf("AF27"), + ), + + COQ( + cards2ResId = R.drawable.ill_coq_card2_120_106, + cards3ResId = R.drawable.ill_coq_card3_120_106, + batchIds = setOf("AF28"), + ), + + CryptoSeth( + cards2ResId = R.drawable.ill_crypto_seth_card2_120_106, + cards3ResId = R.drawable.ill_crypto_seth_card3_120_106, + batchIds = setOf("AF32"), + ), + + Grim( + cards2ResId = R.drawable.ill_grim_card2_120_106, + cards3ResId = R.drawable.ill_grim_card3_120_106, + batchIds = setOf("AF13"), + ), + + Jr( + cards2ResId = R.drawable.ill_jr_card2_120_106, + cards3ResId = R.drawable.ill_jr_card3_120_106, + batchIds = setOf("AF14"), + ), + + Kaspa( + cards2ResId = R.drawable.ill_kaspa_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_card3_120_106, + batchIds = setOf("AF08"), + ), + + Kaspa2( + cards2ResId = R.drawable.ill_kaspa2_card2_120_106, + cards3ResId = R.drawable.ill_kaspa2_card3_120_106, + batchIds = setOf("AF25"), + ), + + KaspaReseller( + cards2ResId = R.drawable.ill_kaspa_reseller_card2_120_106, + cards3ResId = R.drawable.ill_kaspa_reseller_card3_120_106, + batchIds = setOf("AF31"), + ), + + KishuInu( + cards2ResId = R.drawable.ill_kishu_inu_card2_120_106, + cards3ResId = R.drawable.ill_kishu_inu_card3_120_106, + batchIds = setOf("AF52"), + ), + + NewWorldElite( + cards2ResId = R.drawable.ill_nwe_card2_120_106, + cards3ResId = R.drawable.ill_nwe_card3_120_106, + batchIds = setOf("AF26"), + ), + + // for multicolored cards use image of 3 cards in all cases + Pastel( + cards2ResId = R.drawable.ill_pastel_cards3_120_106, + cards3ResId = R.drawable.ill_pastel_cards3_120_106, + batchIds = setOf("AF43", "AF44", "AF45"), + ), + + RedPanda( + cards2ResId = R.drawable.ill_red_panda_card2_120_106, + cards3ResId = R.drawable.ill_red_panda_card3_120_106, + batchIds = setOf("AF34"), + ), + + SatoshiFriends( + cards2ResId = R.drawable.ill_satoshi_card2_120_106, + cards3ResId = R.drawable.ill_satoshi_card3_120_106, + batchIds = setOf("AF19"), + ), + + Trillant( + cards2ResId = R.drawable.ill_trillant_card2_120_106, + cards3ResId = R.drawable.ill_trillant_card3_120_106, + batchIds = setOf("AF16"), + ), + + Tron( + cards2ResId = R.drawable.ill_tron_card2_120_106, + cards3ResId = R.drawable.ill_tron_card3_120_106, + batchIds = setOf("AF07"), + ), + + VeChain( + cards2ResId = R.drawable.ill_vechain_card2_120_106, + cards3ResId = R.drawable.ill_vechain_card3_120_106, + batchIds = setOf("AF29"), + ), + + // for multicolored cards use image of 3 cards in all cases + Vivid( + cards2ResId = R.drawable.ill_vivid_cards3_120_106, + cards3ResId = R.drawable.ill_vivid_cards3_120_106, + batchIds = setOf("AF40", "AF41", "AF42"), + ), + + VoltInu( + cards2ResId = R.drawable.ill_volt_inu_card2_120_106, + cards3ResId = R.drawable.ill_volt_inu_card3_120_106, + batchIds = setOf("AF35"), + ), + + WhiteTangem( + cards2ResId = R.drawable.ill_white_card2_120_106, + cards3ResId = R.drawable.ill_white_card3_120_106, + batchIds = setOf("AF15"), + ), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 1f34e460f2..3de82a88d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 722f7de8f5..711cdaac37 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes -import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.demo.DemoConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R @@ -12,7 +12,6 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ -// TODO: make flexible to integrate cobrands ([REDACTED_JIRA]) internal object WalletImageResolver { private const val WALLET_WITHOUT_BACKUP_COUNT = 1 @@ -24,42 +23,42 @@ internal object WalletImageResolver { @DrawableRes fun resolve(userWallet: UserWallet): Int? { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + val cobrandImage = Wallet2CobrandImage.entries.firstOrNull { + it.batchIds.contains(userWallet.scanResponse.card.batchId) + } + + val noteImage by lazy { + val noteBlockchain = cardTypesResolver.getBlockchain() + + NoteImage.entries.firstOrNull { it.blockchain == noteBlockchain } + } + return when { cardTypesResolver.isDevKit() -> R.drawable.ill_dev_120_106 - cardTypesResolver.isWhiteWallet2() -> userWallet.resolveWhiteWallet2() - cardTypesResolver.isAvroraWallet() -> userWallet.resolveAvroraWallet() - cardTypesResolver.isTraillantWallet() -> userWallet.resolveTraillantWallet() - cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() - cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() - cardTypesResolver.isKaspa2Wallet() -> userWallet.resolveKaspa2Wallet() - cardTypesResolver.isKaspaResellerWallet() -> userWallet.resolveKaspaResellerWallet() - cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() - cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() - cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() - cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() - cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() - cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() - cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() - cardTypesResolver.isRedPandaWallet() -> userWallet.resolveRedPandaWallet() - cardTypesResolver.isCryptoSethWallet() -> userWallet.resolveCryptoSethWallet() - cardTypesResolver.isKishuInuWallet() -> userWallet.resolveKishuInuWallet() - cardTypesResolver.isBabyDogeWallet() -> userWallet.resolveBabyDogeWallet() - cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet() - cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet() - cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet() - cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet() - cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet() + cobrandImage != null -> userWallet.resolveWallet2Cobrand(image = cobrandImage) cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() cardTypesResolver.isWhiteWallet() -> R.drawable.ill_wallet_old_white_120_106 cardTypesResolver.isTangemTwins() -> R.drawable.ill_twins_120_106 cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106 - cardTypesResolver.isTangemNote() -> resolveNote(blockchain = cardTypesResolver.getBlockchain()) + cardTypesResolver.isTangemNote() -> noteImage?.imageResId else -> null } } + private fun UserWallet.resolveWallet2Cobrand(image: Wallet2CobrandImage): Int? { + return resolveWallet2(oneBackupResId = image.cards2ResId, twoBackupResId = image.cards3ResId) + } + + private fun UserWallet.resolveShibaWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_shiba_card2_120_106, + twoBackupResId = R.drawable.ill_shiba_card3_120_106, + ) + } + private fun UserWallet.resolveWallet2( @DrawableRes oneBackupResId: Int = R.drawable.ill_wallet2_cards2_120_106, @DrawableRes twoBackupResId: Int = R.drawable.ill_wallet2_cards3_120_106, @@ -75,176 +74,6 @@ internal object WalletImageResolver { } } - private fun UserWallet.resolveTronWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_tron_card2_120_106, - twoBackupResId = R.drawable.ill_tron_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspa2Wallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa2_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa2_card3_120_106, - ) - } - - private fun UserWallet.resolveKaspaResellerWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kaspa_reseller_card2_120_106, - twoBackupResId = R.drawable.ill_kaspa_reseller_card3_120_106, - ) - } - - private fun UserWallet.resolveBadWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_bad_card2_120_106, - twoBackupResId = R.drawable.ill_bad_card3_120_106, - ) - } - - private fun UserWallet.resolveJrWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_jr_card2_120_106, - twoBackupResId = R.drawable.ill_jr_card3_120_106, - ) - } - - private fun UserWallet.resolveGrimWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_grim_card2_120_106, - twoBackupResId = R.drawable.ill_grim_card3_120_106, - ) - } - - private fun UserWallet.resolveSatoshiWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_satoshi_card2_120_106, - twoBackupResId = R.drawable.ill_satoshi_card3_120_106, - ) - } - - private fun UserWallet.resolveShibaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_shiba_card2_120_106, - twoBackupResId = R.drawable.ill_shiba_card3_120_106, - ) - } - - private fun UserWallet.resolveWhiteWallet2(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_white_card2_120_106, - twoBackupResId = R.drawable.ill_white_card3_120_106, - ) - } - - private fun UserWallet.resolveAvroraWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_avrora_card2_120_106, - twoBackupResId = R.drawable.ill_avrora_card3_120_106, - ) - } - - private fun UserWallet.resolveTraillantWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_traillant_card2_120_106, - twoBackupResId = R.drawable.ill_traillant_card3_120_106, - ) - } - - private fun UserWallet.resolveBitcoinPizzaDayWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_pizza_day_card2_120_106, - twoBackupResId = R.drawable.ill_pizza_day_card3_120_106, - ) - } - - private fun UserWallet.resolveVeChainWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_vechain_card2_120_106, - twoBackupResId = R.drawable.ill_vechain_card3_120_106, - ) - } - - private fun UserWallet.resolveNewWorldEliteWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_nwe_card2_120_106, - twoBackupResId = R.drawable.ill_nwe_card3_120_106, - ) - } - - private fun UserWallet.resolveRedPandaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_red_panda_card2_120_106, - twoBackupResId = R.drawable.ill_red_panda_card3_120_106, - ) - } - - private fun UserWallet.resolveCryptoSethWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_crypto_seth_card2_120_106, - twoBackupResId = R.drawable.ill_crypto_seth_card3_120_106, - ) - } - - private fun UserWallet.resolveKishuInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_kishu_inu_card2_120_106, - twoBackupResId = R.drawable.ill_kishu_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveBabyDogeWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_baby_doge_card2_120_106, - twoBackupResId = R.drawable.ill_baby_doge_card3_120_106, - ) - } - - private fun UserWallet.resolveCOQWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coq_card2_120_106, - twoBackupResId = R.drawable.ill_coq_card3_120_106, - ) - } - - private fun UserWallet.resolveCoinMetricaWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_coin_metrica_card2_120_106, - twoBackupResId = R.drawable.ill_coin_metrica_card3_120_106, - ) - } - - private fun UserWallet.resolveVoltInuWallet(): Int? { - return resolveWallet2( - oneBackupResId = R.drawable.ill_volt_inu_card2_120_106, - twoBackupResId = R.drawable.ill_volt_inu_card3_120_106, - ) - } - - private fun UserWallet.resolveVividWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_vivid_cards3_120_106, - twoBackupResId = R.drawable.ill_vivid_cards3_120_106, - ) - } - - private fun UserWallet.resolvePastelWallet(): Int? { - // for multicolored cards use image of 3 cards in all cases - return resolveWallet2( - oneBackupResId = R.drawable.ill_pastel_cards3_120_106, - twoBackupResId = R.drawable.ill_pastel_cards3_120_106, - ) - } - private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { @@ -261,16 +90,4 @@ internal object WalletImageResolver { return if (count != null) resolve(count) else null } - - private fun resolveNote(blockchain: Blockchain): Int? { - return when (blockchain) { - Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 - Blockchain.Ethereum -> R.drawable.ill_note_ethereum_120_106 - Blockchain.BSC -> R.drawable.ill_note_binance_120_106 - Blockchain.Dogecoin -> R.drawable.ill_note_doge_120_106 - Blockchain.Cardano -> R.drawable.ill_note_cardano_120_106 - Blockchain.XRP -> R.drawable.ill_note_xrp_120_106 - else -> null - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index 62f9ca68a1..c582f8d8d8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index d42d27f165..d4f7d3db34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -4,9 +4,9 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index fd70fb3cfa..a13bd46297 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 1994185f73..59531e1ece 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 6a914a5c2a..0ff9942daf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 775957b478..8ea62cf3fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index 9abebae38f..e9752df81b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -4,11 +4,11 @@ import com.tangem.common.extensions.isZero import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN @@ -59,7 +59,7 @@ internal class TokenItemStateConverter( hasStaked = !getStakedBalance().isZero(), ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), - cryptoPriceState = getCryptoPriceState(), + subtitleState = getCryptoPriceState(), onItemClick = { clickIntents.onTokenItemClick(this) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) @@ -97,12 +97,12 @@ internal class TokenItemStateConverter( onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange return if (fiatRate != null && priceChange != null) { - TokenItemState.CryptoPriceState.Content( + TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(), priceChangePercent = BigDecimalFormatter.formatPercent( percent = priceChange, @@ -111,7 +111,7 @@ internal class TokenItemStateConverter( type = priceChange.getPriceChangeType(), ) } else { - TokenItemState.CryptoPriceState.Unknown + TokenItemState.SubtitleState.Unknown } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index 48e959ad54..ca9ee260f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.runtime.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.impl.R @@ -21,12 +21,12 @@ internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) { @Composable private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { - val confirmButton: DialogButton - val dismissButton: DialogButton? + val confirmButton: DialogButtonUM + val dismissButton: DialogButtonUM? val onActionClick = state.onConfirmClick if (onActionClick != null) { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = { @@ -34,12 +34,12 @@ private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { onDismiss() }, ) - dismissButton = DialogButton( + dismissButton = DialogButtonUM( title = stringResource(id = R.string.common_cancel), onClick = onDismiss, ) } else { - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), warning = state.isWarningConfirmButton, onClick = onDismiss, @@ -62,7 +62,7 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U TextInputDialog( fieldValue = value, - confirmButton = DialogButton( + confirmButton = DialogButtonUM( title = state.confirmButtonText.resolveReference(), enabled = value.text.isNotEmpty() && value.text != state.text && @@ -75,8 +75,8 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U onDismissDialog = onDismiss, onValueChange = { value = it }, title = state.title.resolveReference(), - dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), - textFieldParams = AdditionalTextInputDialogParams( + dismissButton = DialogButtonUM(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + textFieldParams = AdditionalTextInputDialogUM( label = state.label.resolveReference(), isError = state.errorTextProvider(value.text) != null, caption = state.errorTextProvider(value.text)?.resolveReference(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index b8dd2da1b7..f73b2218a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -71,9 +71,9 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.component.BottomSheetState.* -import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.entry.BottomSheetState +import com.tangem.features.markets.entry.BottomSheetState.* +import com.tangem.features.markets.entry.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 9d39d7fcbc..87d84a3a64 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem -import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 26d3d0c945..835a480b48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import arrow.core.getOrElse +import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 5c192d9a44..6b0d74852e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -90,7 +90,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onManageTokensClick() { analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) - router.openManageTokensScreen() + router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onOrganizeTokensClick() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 98590b9a89..cb24122b08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -223,19 +223,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) viewModelScope.launch(dispatchers.main) { - walletManagersFacade.getAddress( + walletManagersFacade.getDefaultAddress( userWalletId = stateHolder.getSelectedWalletId(), network = cryptoCurrencyStatus.currency.network, - ) - .find { it.type == AddressType.Default } - ?.value - ?.let { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + )?.let { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) - walletEventSender.send( - event = WalletEvent.CopyAddress(address = it), - ) - } + walletEventSender.send( + event = WalletEvent.CopyAddress(address = it), + ) + } } } diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card2_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card2_120_106.webp diff --git a/features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ill_traillant_card3_120_106.webp rename to features/wallet/impl/src/main/res/drawable/ill_trillant_card3_120_106.webp diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt new file mode 100644 index 0000000000..c6da034569 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/NoteImageTest.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NoteImageTest { + + @Test + fun check() { + // check uniqueness + Truth.assertThat(NoteImage.entries.map { it.blockchain }).containsNoDuplicates() + Truth.assertThat(NoteImage.entries.map { it.imageResId }).containsNoDuplicates() + + // check blockchain matching + Truth.assertThat(NoteImage.Bitcoin.blockchain).isEqualTo(Blockchain.Bitcoin) + Truth.assertThat(NoteImage.Ethereum.blockchain).isEqualTo(Blockchain.Ethereum) + Truth.assertThat(NoteImage.Binance.blockchain).isEqualTo(Blockchain.BSC) + Truth.assertThat(NoteImage.Dogecoin.blockchain).isEqualTo(Blockchain.Dogecoin) + Truth.assertThat(NoteImage.Cardano.blockchain).isEqualTo(Blockchain.Cardano) + Truth.assertThat(NoteImage.XRP.blockchain).isEqualTo(Blockchain.XRP) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt new file mode 100644 index 0000000000..13d88a97fd --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImageTest.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.google.common.truth.Truth +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class Wallet2CobrandImageTest { + + @Test + fun checkUniqueness() { + val excluded = listOf(Wallet2CobrandImage.Pastel, Wallet2CobrandImage.Vivid) + (Wallet2CobrandImage.entries - excluded).forEach { + Truth.assertThat(it.cards2ResId != it.cards3ResId).isTrue() + } + + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards2ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.map { it.cards3ResId }).containsNoDuplicates() + Truth.assertThat(Wallet2CobrandImage.entries.flatMap { it.batchIds }).containsNoDuplicates() + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b259900751..45137306fa 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.14-747" +tangemBlockchainSdk = "develop-748" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.14-379" +tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^