diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de5864d8b1..2791ba88f4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.swap.presentation) implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) implementation(projects.features.swap.data) implementation(projects.features.tester.api) implementation(projects.features.tester.impl) diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 1c596a4b4d..7334227d99 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -8,6 +8,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module @@ -103,4 +104,20 @@ internal object SettingsDomainModule { fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) } + + @Provides + @ViewModelScoped + fun provideShouldShowSwapPromoWalletUseCase( + swapPromoRepository: SwapPromoRepository, + ): ShouldShowSwapPromoWalletUseCase { + return ShouldShowSwapPromoWalletUseCase(swapPromoRepository) + } + + @Provides + @ViewModelScoped + fun provideShouldShowSwapPromoTokenUseCase( + swapPromoRepository: SwapPromoRepository, + ): ShouldShowSwapPromoTokenUseCase { + return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) + } } \ No newline at end of file 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 d4488fe919..d62f5001d9 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 @@ -1,9 +1,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -83,6 +85,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + swapRepository: SwapRepository, + showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( @@ -90,6 +95,9 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, + swapRepository = swapRepository, + showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt index 7e24b61c6d..17714e011b 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt @@ -21,6 +21,10 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact ) } + override suspend fun estimateFee(amount: Amount): Result { + return getFee(amount, walletManager.wallet.address) + } + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { val signerResponse = signer.sign( hash = getDataToSign(), diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d10a9dab4f..26d5b3cf08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -42,6 +42,10 @@ object PreferencesKeys { val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") } val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } + + val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") } + + val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 391fe57950..21bec9195d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -244,6 +244,8 @@ 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту Отсканируйте карту + Обменивайте свои цифровые активы между различными сетями + Кроссчейн-своп теперь доступен Токены Добавить Изменить @@ -539,6 +541,9 @@ Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s Нет цены + Обменяйте этот токен на другие активы в вашем портфеле + Представляем кроссчейн-своп + Обменять контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 08c1787e9b..3088fc97ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -263,6 +263,8 @@ 1INCH tokens will be credited to your %s wallet address within 48 hours To access all the networks you need to scan the card Scan your card + Swap multiple currencies across several blockchains + Cross-chain swaps are now available Tokens Add Edit @@ -569,6 +571,9 @@ The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. Unable to hide %s No rate + Exchange this token for other assets in your portfolio + Introducing cross-chain swaps + Exchange now contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 1992043a9e..c6180da951 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -6,12 +6,13 @@ import com.tangem.core.ui.extensions.TextReference /** * Notification component state * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property buttonsState buttons state - * @property onClick lambda be invoked when notification is clicked - * @property onCloseClick lambda be invoked when close button is clicked + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property backgroundResId background resource id + * @property buttonsState buttons state + * @property onClick lambda be invoked when notification is clicked + * @property onCloseClick lambda be invoked when close button is clicked * [REDACTED_AUTHOR] */ @@ -19,6 +20,7 @@ data class NotificationConfig( val title: TextReference, val subtitle: TextReference, @DrawableRes val iconResId: Int, + @DrawableRes val backgroundResId: Int? = null, val buttonsState: ButtonsState? = null, val onClick: (() -> Unit)? = null, val onCloseClick: (() -> Unit)? = null, @@ -33,7 +35,11 @@ data class NotificationConfig( val onClick: () -> Unit, ) : ButtonsState() - data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState() + data class SecondaryButtonConfig( + val text: TextReference, + @DrawableRes val iconResId: Int? = null, + val onClick: () -> Unit, + ) : ButtonsState() data class PairButtonsConfig( val primaryText: TextReference, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt new file mode 100644 index 0000000000..072a7389f6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -0,0 +1,170 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +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.TangemTheme + +/** + * Custom notification with image background + * @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev) + */ +@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") +@Composable +fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + + ConstraintLayout( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium), + ) { + val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs() + + val spacing2 = TangemTheme.dimens.spacing2 + val spacing12 = TangemTheme.dimens.spacing12 + val spacing14 = TangemTheme.dimens.spacing14 + + Image( + painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_banner_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.constrainAs(backgroundRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible + }, + ) + Image( + painter = painterResource(config.iconResId), + contentDescription = null, + modifier = Modifier.constrainAs(iconRef) { + start.linkTo(parent.start, spacing12) + linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) + }, + ) + Text( + text = config.title.resolveReference(), + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier.constrainAs(titleRef) { + top.linkTo(parent.top, spacing12) + start.linkTo(iconRef.end, spacing12) + end.linkTo(closeIconRef.start, spacing2) + width = Dimension.fillToConstraints + }, + ) + Text( + text = config.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier.constrainAs(subtitleRef) { + top.linkTo(titleRef.bottom, spacing2) + start.linkTo(iconRef.end, spacing12) + end.linkTo(parent.end, spacing12) + bottom.linkTo(buttonRef.top, spacing12, spacing14) + width = Dimension.fillToConstraints + }, + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .constrainAs(closeIconRef) { + top.linkTo(parent.top, spacing12) + end.linkTo(parent.end, spacing12) + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { + config.onCloseClick?.invoke() + }, + ) + SecondaryButtonIconStart( + text = button?.text?.resolveReference().orEmpty(), + iconResId = button?.iconResId ?: R.drawable.ic_exchange_vertical_24, + onClick = button?.onClick ?: {}, + modifier = Modifier.constrainAs(buttonRef) { + start.linkTo(parent.start, spacing12) + end.linkTo(parent.end, spacing12) + bottom.linkTo(parent.bottom, spacing12) + width = Dimension.fillToConstraints + visibility = if (button == null) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + } +} + +//region preview +@Preview +@Composable +private fun NotificationWithBackgroundPreview( + @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, +) { + TangemTheme { + NotificationWithBackground(config = config) + } +} + +private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NotificationConfig( + title = resourceReference(id = R.string.main_swap_promotion_title), + subtitle = resourceReference(id = R.string.main_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + ), + NotificationConfig( + title = resourceReference(id = R.string.token_swap_promotion_title), + subtitle = stringReference( + "Swap multiple currencies between any chains you wish. Swap multiple " + + "currencies between any chains you wish. Swap multiple " + + "currencies between any chains you wish. Commission free period " + + "till Dec 31.", + ), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.token_swap_promotion_button), + onClick = {}, + ), + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 202930549a..6b4cc1bd3d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -71,7 +71,6 @@ fun LazyListScope.txHistoryItems( } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, isBalanceHidden: Boolean, @@ -93,7 +92,6 @@ private fun LazyListScope.contentItems( state = item, isBalanceHidden = isBalanceHidden, modifier = modifier - .animateItemPlacement() .roundedShapeItemDecoration( currentIndex = index, lastIndex = txHistoryItems.itemSnapshotList.lastIndex, diff --git a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml new file mode 100644 index 0000000000..3c279910c8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp b/core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp new file mode 100644 index 0000000000..1e952cd6f7 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp differ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt new file mode 100644 index 0000000000..dd94a2e07c --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -0,0 +1,55 @@ +package com.tangem.data.settings + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_SHOW_KEY +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.Calendar + +/** + * Repository for showing swap promo notification. + */ +class DefaultSwapPromoRepository( + private val appPreferencesStore: AppPreferencesStore, +) : SwapPromoRepository { + override fun isReadyToShowWallet(): Flow { + return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_SHOW_KEY, true) + .map { it && checkPromoPeriod() } + } + + override fun isReadyToShowToken(): Flow { + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true) + .map { it && checkPromoPeriod() } + } + + override suspend fun setNeverToShowWallet() { + appPreferencesStore.store( + key = IS_WALLET_SWAP_PROMO_SHOW_KEY, + value = false, + ) + } + + override suspend fun setNeverToShowToken() { + appPreferencesStore.store( + key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, + value = false, + ) + } + + private fun checkPromoPeriod(): Boolean { + val currentDate = Calendar.getInstance() + return currentDate.get(Calendar.DATE) in START_DATE_KEY..END_DATE_KEY && + currentDate.get(Calendar.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY + } + + companion object { + private const val START_DATE_KEY = 15 + private const val END_DATE_KEY = 31 + private const val MONTH_KEY = 11 // December + private const val YEAR_KEY = 2023 // just in case + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index ba83b09d4d..43d5796d15 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -2,10 +2,12 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.data.settings.DefaultSwapPromoRepository import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -36,4 +38,10 @@ internal object SettingsDataModule { fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository { return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore) } + + @Provides + @Singleton + fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository { + return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore) + } } \ 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 d1a57e7726..cdffab603a 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 @@ -38,7 +38,7 @@ import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet -@Suppress("LargeClass") +@Suppress("LargeClass", "TooManyFunctions") // FIXME: Move to its own module and make internal @Deprecated("Inject the WalletManagerFacade interface using DI instead") class DefaultWalletManagersFacade( @@ -433,6 +433,20 @@ class DefaultWalletManagersFacade( ) } + override suspend fun estimateFee( + amount: Amount, + userWalletId: UserWalletId, + network: Network, + ): Result? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as? TransactionSender)?.estimateFee(amount = amount) + } + override suspend fun validateTransaction( amount: Amount, fee: Amount?, 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 dba5d089eb..548668c5a7 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 @@ -25,6 +25,7 @@ import java.util.EnumSet /** * A facade for managing wallets. */ +@Suppress("TooManyFunctions") interface WalletManagersFacade { /** @@ -166,6 +167,15 @@ interface WalletManagersFacade { network: Network, ): Result? + /** + * Returns estimated fee for transaction + * + * @param amount of transaction + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun estimateFee(amount: Amount, userWalletId: UserWalletId, network: Network): Result? + /** * Validates transaction * diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt new file mode 100644 index 0000000000..0c75a4f52a --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { + + operator fun invoke(): Flow = swapPromoRepository.isReadyToShowToken() + + suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt new file mode 100644 index 0000000000..573a5ea94b --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) { + + operator fun invoke(): Flow = swapPromoRepository.isReadyToShowWallet() + + suspend fun neverToShow() = swapPromoRepository.setNeverToShowWallet() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt new file mode 100644 index 0000000000..95d31f65d8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings.repositories + +import kotlinx.coroutines.flow.Flow + +interface SwapPromoRepository { + fun isReadyToShowWallet(): Flow + + fun isReadyToShowToken(): Flow + + suspend fun setNeverToShowWallet() + + suspend fun setNeverToShowToken() +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 8fabb669f0..e32512bb66 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -18,6 +18,9 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.settings) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index f6c14d8482..d818e80eea 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -31,4 +31,6 @@ sealed class CryptoCurrencyWarning { data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning() + + object SwapPromo : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt new file mode 100644 index 0000000000..cc39985633 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.models.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class TokenSwapPromoAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Swap Promo", event, params, null) { + + object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close") + + class Exchange( + token: String, + ) : TokenSwapPromoAnalyticsEvent( + event = "Button - Exchange Now", + params = mapOf("Token" to token), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 0ff4f92ae7..f658706441 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,24 +1,33 @@ package com.tangem.domain.tokens +import arrow.core.Either +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import java.math.BigDecimal +@Suppress("LongParameterList") class GetCurrencyWarningsUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val swapRepository: SwapRepository, + private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -29,20 +38,32 @@ class GetCurrencyWarningsUseCase( isSingleWalletWithTokens: Boolean, ): Flow> { val currency = currencyStatus.currency + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) return combine( getCoinRelatedWarnings( - userWalletId = userWalletId, + operations = operations, networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), + operations.getCurrenciesStatusesFlow().conflate(), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), - flowOf(getNetworkUnavailableWarning(currencyStatus)), - flowOf(getNetworkNoAccountWarning(currencyStatus)), - ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount -> + showSwapPromoTokenUseCase().conflate(), + ) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo -> setOfNotNull( + getSwapPromoNotificationWarning( + shouldShowSwapPromo = shouldShowSwapPromo, + userWalletId = userWalletId, + currencyStatus = currencyStatus, + cryptoStatuses = cryptoStatuses, + ), maybeRentWarning, maybeEdWarning?.let { CryptoCurrencyWarning.ExistentialDeposit( @@ -51,27 +72,69 @@ class GetCurrencyWarningsUseCase( ) }, *coinRelatedWarnings.toTypedArray(), - maybeNetworkUnavailable, - maybeNetworkNoAccount, + getNetworkUnavailableWarning(currencyStatus), + getNetworkNoAccountWarning(currencyStatus), ) }.flowOn(dispatchers.io) } + private suspend fun getSwapPromoNotificationWarning( + shouldShowSwapPromo: Boolean, + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + cryptoStatuses: Either>, + ): CryptoCurrencyWarning? { + val currency = currencyStatus.currency + return if (shouldShowSwapPromo && marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) { + cryptoStatuses.fold( + ifLeft = { null }, + ifRight = { cryptoCurrencyStatuses -> + val pairs = swapRepository.getPairs( + LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ), + cryptoCurrencyStatuses.map { it.currency }, + ) + val filteredCurrencies = cryptoCurrencyStatuses.filterNot { + it.currency.network.backendId == currency.network.backendId + } + val currencyPairs = pairs.pairs.filter { + it.from.network == currency.network.backendId || + it.to.network == currency.network.backendId + } + val isExchangeable = currencyPairs.any { pair -> + val availablePair = if (currencyStatus.value.amount.isZero()) { + filteredCurrencies.filterNot { it.value.amount.isZero() } + } else { + filteredCurrencies + } + availablePair + .any { + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network + } + } + if (isExchangeable) { + CryptoCurrencyWarning.SwapPromo + } else { + null + } + }, + ) + } else { + null + } + } + @Suppress("LongParameterList") private suspend fun getCoinRelatedWarnings( - userWalletId: UserWalletId, + operations: CurrenciesStatusesOperations, networkId: Network.ID, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - userWalletId = userWalletId, - ) - val currencyFlow = if (isSingleWalletWithTokens) { operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) } else { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt new file mode 100644 index 0000000000..28f48358a0 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.math.BigDecimal + +/** + * Use case to estimate transaction fee + */ +class EstimateFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val dispatcher: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + amount: BigDecimal, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Flow> { + return flow { + val result = walletManagersFacade.estimateFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + + val maybeFee = when (result) { + is Result.Success -> result.data.right() + else -> GetFeeError.DataError.left() + } + emit(maybeFee) + }.flowOn(dispatcher.io) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 53dddfc5a9..a2b37ee319 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.models) + implementation(projects.features.swap.domain.api) /** Network */ implementation(deps.retrofit) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 26ac346f90..ffd12827ba 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,8 +1,10 @@ package com.tangem.feature.swap import arrow.core.Either +import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.right import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain @@ -27,12 +29,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -184,7 +185,7 @@ internal class SwapRepositoryImpl @Inject constructor( toDecimals: Int, providerId: String, rateType: RateType, - ): AggregatedSwapDataModel { + ): Either { return withContext(coroutineDispatcher.io) { try { val response = tangemExpressApi.getExchangeQuote( @@ -198,14 +199,12 @@ internal class SwapRepositoryImpl @Inject constructor( providerId = providerId, rateType = rateType.name.lowercase(), ).getOrThrow() - AggregatedSwapDataModel( - dataModel = QuoteModel( - toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), - allowanceContract = response.allowanceContract, - ), - ) + QuoteModel( + toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), + allowanceContract = response.allowanceContract, + ).right() } catch (ex: Exception) { - AggregatedSwapDataModel(null, getDataError(ex)) + getDataError(ex).left() } } } @@ -227,7 +226,7 @@ internal class SwapRepositoryImpl @Inject constructor( providerId: String, rateType: RateType, toAddress: String, - ): AggregatedSwapDataModel { + ): Either { return withContext(coroutineDispatcher.io) { try { val response = tangemExpressApi.getExchangeData( @@ -242,11 +241,9 @@ internal class SwapRepositoryImpl @Inject constructor( rateType = rateType.name.lowercase(), toAddress = toAddress, ).getOrThrow() - AggregatedSwapDataModel( - dataModel = expressDataConverter.convert(response), - ) + expressDataConverter.convert(response).right() } catch (ex: Exception) { - AggregatedSwapDataModel(null, getDataError(ex)) + getDataError(ex).left() } } } 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 b30b20f817..83ddab6b62 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 @@ -13,7 +13,7 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.converters.ErrorsDataConverter import com.tangem.feature.swap.DefaultSwapTransactionRepository -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module diff --git a/features/swap/domain/api/.gitignore b/features/swap/domain/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/domain/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/domain/api/build.gradle.kts b/features/swap/domain/api/build.gradle.kts new file mode 100644 index 0000000000..38ecfe752e --- /dev/null +++ b/features/swap/domain/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.swap.domain.api" +} + +dependencies { + implementation(projects.features.swap.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(deps.arrow.core) + +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt similarity index 91% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt rename to features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index f68c41e255..ea82fe6495 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -1,10 +1,10 @@ -package com.tangem.feature.swap.domain +package com.tangem.feature.swap.domain.api import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.DataError import java.math.BigDecimal interface SwapRepository { @@ -28,7 +28,7 @@ interface SwapRepository { toDecimals: Int, providerId: String, rateType: RateType, - ): AggregatedSwapDataModel + ): Either /** * Returns address of 1inch router that must be trusted @@ -77,7 +77,7 @@ interface SwapRepository { providerId: String, rateType: RateType, toAddress: String, - ): AggregatedSwapDataModel + ): Either fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b1a50d2773..5fa5dfec86 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -29,6 +29,8 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory.models) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Core modules */ implementation(projects.core.utils) @@ -38,7 +40,6 @@ dependencies { implementation(projects.features.wallet.api) /** Other Libraries **/ - implementation(deps.kotlin.serialization) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.timber) diff --git a/features/swap/domain/models/.gitignore b/features/swap/domain/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/domain/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/domain/models/build.gradle.kts b/features/swap/domain/models/build.gradle.kts new file mode 100644 index 0000000000..96958add9b --- /dev/null +++ b/features/swap/domain/models/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.feature.swap.domain.models" +} + +dependencies { + /** Domain */ + implementation(projects.domain.tokens.models) + + /** Core modules */ + implementation(projects.core.utils) + + /** Other Libraries **/ + implementation(deps.kotlin.serialization) + implementation(deps.arrow.core) + +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index d36610f673..765647a01e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -5,7 +5,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.* interface SwapInteractor { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f15db9df11..3754e0c835 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain +import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -13,15 +14,16 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade 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.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter +import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* @@ -54,8 +56,8 @@ internal class SwapInteractorImpl @Inject constructor( private val initialToCurrencyResolver: InitialToCurrencyResolver, ) : SwapInteractor { - private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { - GetFeeUseCase(walletManagersFacade, dispatcher) + private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { + EstimateFeeUseCase(walletManagersFacade, dispatcher) } private val swapCurrencyConverter = SwapCurrencyConverter() @@ -297,13 +299,14 @@ internal class SwapInteractorImpl @Inject constructor( ) val fromTokenAddress = getTokenAddress(fromToken.currency) - val isAllowedToSpend = if (quotes.dataModel != null) { - quotes.dataModel.allowanceContract?.let { - isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: true - } else { - false - } + val isAllowedToSpend = quotes.fold( + ifRight = { + it.allowanceContract?.let { + isAllowedToSpend(networkId, fromToken.currency, amount, it) + } ?: true + }, + ifLeft = { false }, + ) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) @@ -497,13 +500,13 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", - ) + ).getOrNull() val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction(txFee), memo = null, - destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo, + destination = (exchangeData?.transaction as ExpressTransactionModel.CEX).txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, ) @@ -514,7 +517,7 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - val externalUrl = (exchangeData.dataModel.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl + val externalUrl = (exchangeData.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl return result.fold( ifLeft = { @@ -533,7 +536,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet = currencyToGet, amount = amount, swapProvider = swapProvider, - swapDataModel = exchangeData.dataModel, + swapDataModel = exchangeData, timestamp = timestamp, ) storeLastCryptoCurrencyId(currencyToGet.currency) @@ -544,10 +547,10 @@ internal class SwapInteractorImpl @Inject constructor( ), fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( - exchangeData.dataModel.toTokenAmount, + exchangeData.toTokenAmount, currencyToGet.currency.symbol, ), - toAmountValue = exchangeData.dataModel.toTokenAmount.value, + toAmountValue = exchangeData.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, @@ -751,7 +754,7 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, - quoteDataModel: AggregatedSwapDataModel, + quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, @@ -761,58 +764,60 @@ internal class SwapInteractorImpl @Inject constructor( txFee: TxFeeState, includeFeeInAmount: IncludeFeeInAmount, ): SwapState { - val quoteModel = quoteDataModel.dataModel - if (quoteModel != null) { - val swapState = updateBalances( - networkId = networkId, - fromTokenStatus = fromToken, - toTokenStatus = toToken, - fromTokenAmount = amount, - toTokenAmount = quoteModel.toTokenAmount, - swapData = null, - txFeeState = txFee, - ) + return quoteDataModel.fold( + ifRight = { quoteModel -> + val swapState = updateBalances( + networkId = networkId, + fromTokenStatus = fromToken, + toTokenStatus = toToken, + fromTokenAmount = amount, + toTokenAmount = quoteModel.toTokenAmount, + swapData = null, + txFeeState = txFee, + ) - return when (exchangeProviderType) { - ExchangeProviderType.DEX -> { - val state = updatePermissionState( - networkId = networkId, - fromTokenStatus = fromToken, - swapAmount = amount, - quotesLoadedState = swapState, - isAllowedToSpend = isAllowedToSpend, - spenderAddress = quoteModel.allowanceContract, - ) - state.copy( - preparedSwapConfigState = state.preparedSwapConfigState.copy( + when (exchangeProviderType) { + ExchangeProviderType.DEX -> { + val state = updatePermissionState( + networkId = networkId, + fromTokenStatus = fromToken, + swapAmount = amount, + quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) + spenderAddress = quoteModel.allowanceContract, + ) + state.copy( + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) + } + ExchangeProviderType.CEX -> { + swapState.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = PreparedSwapConfigState( + isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), + includeFeeInAmount = includeFeeInAmount, + ), + ) + } } - ExchangeProviderType.CEX -> { - swapState.copy( - permissionState = PermissionDataState.Empty, - preparedSwapConfigState = PreparedSwapConfigState( - isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - hasOutgoingTransaction = hasOutgoingTransaction(fromToken), - includeFeeInAmount = includeFeeInAmount, - ), - ) - } - } - } else { - val rates = getQuotes(fromToken.currency.id) - val fromTokenSwapInfo = TokenSwapInfo( - tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) - ?: BigDecimal.ZERO, - cryptoCurrencyStatus = fromToken, - ) - return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error) - } + }, + ifLeft = { error -> + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError(fromTokenSwapInfo, error) + }, + ) } private suspend fun getIncludeFeeInAmount( @@ -882,7 +887,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, selectedFee: FeeType, ): SwapState { - repository.getExchangeData( + return repository.getExchangeData( fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), @@ -893,9 +898,8 @@ internal class SwapInteractorImpl @Inject constructor( providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "", - ).let { - val swapData = it.dataModel - if (swapData != null) { + ).fold( + ifRight = { swapData -> val feeData = transactionManager.getFee( networkId = networkId, amountToSend = amount.value, @@ -926,7 +930,7 @@ internal class SwapInteractorImpl @Inject constructor( swapData = swapData, txFeeState = txFeeState, ) - return swapState.copy( + swapState.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, @@ -936,7 +940,8 @@ internal class SwapInteractorImpl @Inject constructor( includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ), ) - } else { + }, + ifLeft = { error -> val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, @@ -944,12 +949,12 @@ internal class SwapInteractorImpl @Inject constructor( ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) - return SwapState.SwapError( + SwapState.SwapError( fromTokenSwapInfo, - it.error, + error, ) - } - } + }, + ) } @Suppress("LongParameterList") @@ -999,9 +1004,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, ): TxFeeState { getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> - val txFeeResult = getFeeUseCase( + val txFeeResult = estimateFeeUseCase( amount = amount.value, - destination = fromToken.value.networkAddress?.defaultAddress?.value ?: "", userWalletId = userWalletId, cryptoCurrency = fromToken.currency, ).firstOrNull() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index f6bf9093c9..d299f7824e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt deleted file mode 100644 index 45a895181e..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.domain.models.data - -import com.tangem.feature.swap.domain.models.DataError - -/** - * Model that aggregate data model from repository return with error [DataError] if it exists - * - * @param T model type - * @property dataModel - * @property error possible from repository [DataError] - */ -data class AggregatedSwapDataModel( - val dataModel: T?, - val error: DataError = DataError.UnknownError, -) \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index b2ea5cda48..db0e737edf 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -26,6 +26,10 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.settings) + implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** AndroidX */ implementation(deps.androidx.activity.compose) @@ -48,11 +52,6 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) - /** Domain */ - implementation(projects.features.swap.domain) - implementation(projects.domain.tokens.models) - implementation(projects.domain.settings) - /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.kotlin.serialization) 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 2837f48f2a..18933ba236 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 @@ -651,9 +651,17 @@ internal class StateBuilder( } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { + val warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }.toMutableList() + warnings.add( + 0, + SwapWarning.TransactionInProgressWarning( + title = resourceReference(R.string.swapping_pending_transaction_title), + description = resourceReference(R.string.swapping_pending_transaction_subtitle), + ), + ) return uiState.copy( permissionState = SwapPermissionState.InProgress, - warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }, + warnings = warnings, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b17c398d2e..a36badc51e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen @@ -560,6 +560,7 @@ internal class SwapViewModel @Inject constructor( is TxState.TxSent -> { uiState = stateBuilder.loadingPermissionState(uiState) uiState = stateBuilder.dismissBottomSheet(uiState) + startLoadingQuotesFromLastState(isSilent = true) } is TxState.UserCancelled -> Unit else -> { diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 8c4c106fa0..860b64eec4 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -70,9 +70,10 @@ dependencies { /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) - implementation(projects.features.swap.domain) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 5e42550ab6..d55821009b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -1,12 +1,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.collections.immutable.ImmutableList internal data class SwapTransactionsState( val txId: String, @@ -14,10 +15,10 @@ internal data class SwapTransactionsState( val txUrl: String? = null, val timestamp: TextReference, val fiatSymbol: String, - val activeStatus: MutableStateFlow, - val hasFailed: MutableStateFlow, - val statuses: MutableStateFlow>, - val notification: MutableStateFlow = MutableStateFlow(null), + val activeStatus: ExchangeStatus?, + val hasFailed: Boolean, + val statuses: ImmutableList, + val notification: ExchangeStatusNotifications? = null, val toCryptoCurrencyId: CryptoCurrency.ID, val toCryptoAmount: String, val toCryptoSymbol: String, @@ -32,6 +33,7 @@ internal data class SwapTransactionsState( val onGoToProviderClick: (String) -> Unit, ) +@Immutable internal class ExchangeStatusState( val status: ExchangeStatus, val text: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 989aa84d9b..fe9ef56abc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -39,6 +39,23 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) + data class SwapPromo( + val onSwapClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.token_swap_promotion_title), + subtitle = resourceReference(id = R.string.token_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + onCloseClick = onCloseClick, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button), + onClick = onSwapClick, + ), + ), + ) + object NetworksUnreachable : Warning( title = resourceReference(R.string.warning_network_unreachable_title), subtitle = resourceReference(R.string.warning_network_unreachable_message), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index d9362571ca..de2e921017 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -45,6 +45,10 @@ internal class TokenDetailsNotificationConverter( is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions( coinSymbol = warning.blockchainSymbol, ) + is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo( + onSwapClick = clickIntents::onSwapPromoClick, + onCloseClick = clickIntents::onSwapPromoDismiss, + ) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 2c2330ba57..39b31fb3f0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -34,6 +34,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +@Suppress("TooManyFunctions") internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, @@ -247,6 +248,19 @@ internal class TokenDetailsStateFactory( ) } + fun updateStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TangemBottomSheetConfig? { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig + val currentConfig = bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return bottomSheetConfig + return bottomSheetConfig.copy( + content = if (currentConfig.value != swapTxState) { + ExchangeStatusBottomSheetConfig(swapTxState) + } else { + currentConfig + }, + ) + } + fun getStateAndTriggerEvent( state: TokenDetailsState, errorMessage: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 3650c86759..fd13879bc9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -20,11 +20,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update import java.math.BigDecimal internal class TokenDetailsSwapTransactionsStateConverter( @@ -70,14 +69,12 @@ internal class TokenDetailsSwapTransactionsStateConverter( txUrl = transaction.status?.txUrl, timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), fiatSymbol = appCurrency.symbol, - statuses = MutableStateFlow(getStatuses(transaction.status?.status)), - hasFailed = MutableStateFlow(transaction.status?.status == ExchangeStatus.Failed), - activeStatus = MutableStateFlow(transaction.status?.status), - notification = MutableStateFlow( - getNotification( - transaction.status?.status, - transaction.status?.txUrl, - ), + statuses = getStatuses(transaction.status?.status), + hasFailed = transaction.status?.status == ExchangeStatus.Failed, + activeStatus = transaction.status?.status, + notification = getNotification( + transaction.status?.status, + transaction.status?.txUrl, ), toCryptoCurrencyId = toCurrency.currency.id, toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( @@ -109,13 +106,16 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?) { - if (statusModel == null || tx.activeStatus.value == statusModel.status) return - val hasFailed = tx.hasFailed.value || statusModel.status == ExchangeStatus.Failed - tx.activeStatus.update { statusModel.status } - tx.hasFailed.update { hasFailed } - tx.notification.update { getNotification(statusModel.status, statusModel.txUrl) } - tx.statuses.update { getStatuses(statusModel.status, hasFailed) } + fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { + if (statusModel == null || tx.activeStatus == statusModel.status) return tx + val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed + return tx.copy( + activeStatus = statusModel.status, + hasFailed = hasFailed, + notification = getNotification(statusModel.status, statusModel.txUrl), + statuses = getStatuses(statusModel.status, hasFailed), + txUrl = statusModel.txUrl, + ) } private fun getFiatAmount(toFiatAmount: BigDecimal): String { @@ -149,8 +149,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } - private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): List { - if (status == null) return emptyList() + private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { + if (status == null) return persistentListOf() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting val isConfirming = status == ExchangeStatus.Confirming val isVerifying = status == ExchangeStatus.Verifying @@ -181,7 +181,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( isRefunded = isRefunded, hasFailed = hasFailed, ), - ) + ).toPersistentList() } private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( 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 57f5e40688..1f1ae20a89 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 @@ -28,6 +28,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationWithBackground import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.event.EventEffect @@ -104,14 +105,21 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { key = { it::class.java }, contentType = { it.config::class.java }, itemContent = { - Notification( - modifier = itemModifier.animateItemPlacement(), - config = it.config, - iconTint = when (it) { - is TokenDetailsNotification.Warning -> null - is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent - }, - ) + if (it is TokenDetailsNotification.SwapPromo) { + NotificationWithBackground( + config = it.config, + modifier = itemModifier.animateItemPlacement(), + ) + } else { + Notification( + modifier = itemModifier.animateItemPlacement(), + config = it.config, + iconTint = when (it) { + is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + else -> null + }, + ) + } }, ) if (state.isMarketPriceAvailable) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 9d44f11a9e..1574bb2db6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -18,23 +18,21 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState import com.tangem.features.tokendetails.impl.R -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.collections.immutable.ImmutableList @Composable internal fun ExchangeStatusBlock( - statuses: MutableStateFlow>, + statuses: ImmutableList, showLink: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val statusValues = statuses.collectAsStateWithLifecycle() Column( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -77,10 +75,10 @@ internal fun ExchangeStatusBlock( } } - statusValues.value.forEachIndexed { index, item -> + statuses.forEachIndexed { index, item -> ExchangeStatusStep( stepStatus = item, - isLast = index == statusValues.value.lastIndex, + isLast = index == statuses.lastIndex, ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index f8850738e2..7cd58e238d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -36,8 +35,6 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { val config = content.value - val status = config.activeStatus.collectAsStateWithLifecycle() - val notification = config.notification.collectAsStateWithLifecycle() Column( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { @@ -77,15 +74,15 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, - showLink = notification.value == null && config.txUrl != null, + showLink = config.notification == null && config.txUrl != null, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) AnimatedContent( - targetState = notification.value, + targetState = config.notification, label = "Exchange Status Notification Change", ) { it?.let { - val tint = when (status.value) { + val tint = when (config.activeStatus) { ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention ExchangeStatus.Failed -> TangemTheme.colors.icon.warning else -> null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index c73e3e3051..c32011f436 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.constraintlayout.compose.* -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon @@ -41,8 +40,7 @@ internal fun LazyListScope.swapTransactionsItems( contentType = { swapTxs[it]::class.java }, ) { val item = swapTxs[it] - val status = item.activeStatus.collectAsStateWithLifecycle() - val (iconRes, tint) = when (status.value) { + val (iconRes, tint) = when (item.activeStatus) { ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning else -> null to null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 13445dc76d..7ef7622a82 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -12,8 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel @@ -81,8 +81,9 @@ internal class ExchangeStatusFactory( swapTxList.map { tx -> async { val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter.updateTxStatus(tx, statusModel) - tx.removeIfFinished(statusModel?.status) + swapTransactionsStateConverter + .updateTxStatus(tx, statusModel) + .removeIfFinished(statusModel?.status) } } .awaitAll() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 95093f7fdb..d049e3bf31 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.domain.tokens.model.CryptoCurrency +@Suppress("TooManyFunctions") interface TokenDetailsClickIntents { fun onBackClick() @@ -42,4 +43,8 @@ interface TokenDetailsClickIntents { fun onSwapTransactionClick(txId: String) fun onGoToProviderClick(url: String) + + fun onSwapPromoDismiss() + + fun onSwapPromoClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1a718c9846..100aabe066 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -28,6 +29,7 @@ import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -35,12 +37,13 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.* @@ -74,6 +77,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val swapTransactionStatusStore: SwapTransactionStatusStore, @@ -226,7 +230,14 @@ internal class TokenDetailsViewModel @Inject constructor( } }, onSuccess = { updatedTxs -> - uiState = uiState.copy(swapTxs = updatedTxs) + val config = uiState.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig + val currentTx = updatedTxs.firstOrNull { it.txId == config?.value?.txId } + uiState = uiState.copy( + swapTxs = updatedTxs, + bottomSheetConfig = currentTx?.let( + stateFactory::updateStateWithExchangeStatusBottomSheet, + ), + ) }, onError = {}, ), @@ -547,6 +558,18 @@ internal class TokenDetailsViewModel @Inject constructor( router.openUrl(url) } + override fun onSwapPromoDismiss() { + viewModelScope.launch(dispatchers.main) { + shouldShowSwapPromoTokenUseCase.neverToShow() + analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close) + } + } + + override fun onSwapPromoClick() { + onSwapClick() + analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) + } + private companion object { const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 331ccc1502..6aeebbc6c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -166,4 +166,16 @@ sealed class WalletNotification(val config: NotificationConfig) { onCloseClick = onCloseClick, ), ) + + data class SwapPromo( + val onCloseClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.main_swap_promotion_title), + subtitle = resourceReference(id = R.string.main_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + onCloseClick = onCloseClick, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 9e0f9f5123..f258a71ce9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -24,17 +25,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning - is WalletNotification.Informational -> TangemTheme.colors.icon.accent - is WalletNotification.RateApp -> TangemTheme.colors.icon.attention - is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 - is WalletNotification.Warning -> null - }, - ) + if (it is WalletNotification.SwapPromo) { + NotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } else { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + iconTint = when (it) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + else -> null + }, + ) + } }, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 5c390dcb0b..dad5efb741 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -72,4 +72,6 @@ internal interface WalletClickIntents { fun onExploreClick() fun onTransactionClick(txHash: String) + + fun onCloseSwapPromoNotificationClick() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 6540090c11..c0145c919b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -5,6 +5,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.tokens.GetMissedAddressesCryptoCurrenciesUseCase import com.tangem.domain.tokens.error.GetCurrenciesError import com.tangem.domain.tokens.model.CryptoCurrency @@ -26,16 +27,21 @@ import kotlinx.coroutines.flow.flowOf * @property isDemoCardUseCase use case that checks if card is demo * @property isReadyToShowRateAppUseCase use case that checks if card is user already rate app * @property isNeedToBackupUseCase use case that checks if wallet need backup cards + * @property getMissedAddressCryptoCurrenciesUseCase use case that gets missed address crypto currencies + * @property hasSingleWalletSignedHashesUseCase use case that checks if single wallet signed hashes + * @property shouldShowSwapPromoWalletUseCase use case that checks if should show swap promo * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class WalletNotificationsListFactory( private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val getMissedAddressCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, + private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val clickIntents: WalletClickIntents, ) { @@ -51,9 +57,12 @@ internal class WalletNotificationsListFactory( flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(selectedWallet.walletId).conflate(), flow4 = getMissedAddressCryptoCurrenciesUseCase(selectedWallet.walletId).conflate(), - ) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies -> + flow5 = shouldShowSwapPromoWalletUseCase().conflate(), + ) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies, isShowSwapPromo -> readyForRateAppNotification = true buildList { + addSwapPromoNotification(isShowSwapPromo, cardTypesResolver) + addCriticalNotifications(cardTypesResolver) addInformationalNotifications(cardTypesResolver, maybeMissedAddressCurrencies) @@ -77,6 +86,18 @@ internal class WalletNotificationsListFactory( } } + private fun MutableList.addSwapPromoNotification( + showSwapPromo: Boolean, + cardTypesResolver: CardTypesResolver, + ) { + addIf( + element = WalletNotification.SwapPromo( + clickIntents::onCloseSwapPromoNotificationClick, + ), + condition = showSwapPromo && cardTypesResolver.isMultiwalletAllowed(), + ) + } + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { addIf( element = WalletNotification.Critical.DevCard, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 6534df2c8c..7d3c8f1dab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -128,6 +128,7 @@ internal class WalletViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler, + private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isNeedToBackupUseCase: IsNeedToBackupUseCase, getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, @@ -146,6 +147,7 @@ internal class WalletViewModel @Inject constructor( isNeedToBackupUseCase = isNeedToBackupUseCase, getMissedAddressCryptoCurrenciesUseCase = getMissedAddressesCryptoCurrenciesUseCase, hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase, + shouldShowSwapPromoWalletUseCase = shouldShowSwapPromoWalletUseCase, clickIntents = this, ) @@ -774,6 +776,12 @@ internal class WalletViewModel @Inject constructor( refreshSingleCurrencyContent(selectedWalletIndex) } + override fun onCloseSwapPromoNotificationClick() { + viewModelScope.launch(dispatchers.main) { + shouldShowSwapPromoWalletUseCase.neverToShow() + } + } + // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() private fun refreshSingleCurrencyContent(walletIndex: Int) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0df8cabe1b..5557fb257d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-416" +tangemBlockchainSdk = "release-app_5.4-417" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ab3775f15..2d7d4f7efc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -79,12 +79,16 @@ include(":libs:auth") // region Feature modules include(":features:onboarding") + include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") + include(":features:swap:api") include(":features:swap:data") include(":features:swap:domain") +include(":features:swap:domain:models") +include(":features:swap:domain:api") include(":features:swap:presentation") include(":features:tester:api")