diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index a151bfa469..b8c7e6dfc7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -199,4 +199,30 @@ internal object YieldSupplyDomainModule { yieldSupplyRepository = yieldSupplyRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyGetShouldShowMainPromoUseCase { + return YieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplySetShouldShowMainPromoUseCase { + return YieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase { + return YieldSupplyGetDustMinAmountUseCase() + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 639e9a9910..ce46ff400f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -43,12 +43,14 @@ import java.math.BigDecimal */ class TokenItemStateConverter( private val appCurrency: AppCurrency, - private val yieldModuleApyMap: Map = emptyMap(), + private val yieldModuleApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val yieldSupplyPromoBannerKey: String? = null, private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) }, private val onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)? = null, + private val onYieldPromoCloseClick: (() -> Unit)? = null, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus -> createTitleState( currencyStatus = currencyStatus, @@ -66,6 +68,15 @@ class TokenItemStateConverter( private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { createFiatAmountState(status = it, appCurrency = appCurrency) }, + private val promoBannerProvider: (CryptoCurrencyStatus) -> TokenItemState.PromoBannerState = { status -> + createPromoBannerState( + status = status, + yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey, + onApyLabelClick = onApyLabelClick, + onYieldPromoCloseClick = onYieldPromoCloseClick, + ) + }, private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, ) : Converter { @@ -102,6 +113,7 @@ class TokenItemStateConverter( subtitleState = requireNotNull(subtitleStateProvider(this)), fiatAmountState = requireNotNull(fiatAmountStateProvider(this)), subtitle2State = requireNotNull(subtitle2StateProvider(this)), + promoBannerState = promoBannerProvider(this), onItemClick = onItemClick?.let { onItemClick -> { onItemClick(it, this) } }, @@ -164,7 +176,7 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?, ): TokenItemState.TitleState { @@ -204,7 +216,7 @@ class TokenItemStateConverter( // polygon-pos_0xc2132d05d31c914a87c6611c10748aeb04b58e8f private fun resolveEarnApy( cryptoCurrencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, ): EarnApyInfo? { val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token @@ -223,7 +235,7 @@ class TokenItemStateConverter( wrappedList(yieldSupplyApy), ), isActive = isActive, - apy = yieldSupplyApy, + apy = yieldSupplyApy.toString(), source = ApySource.YIELD_SUPPLY, ) } @@ -381,6 +393,36 @@ class TokenItemStateConverter( } } + private fun createPromoBannerState( + status: CryptoCurrencyStatus, + yieldModuleApyMap: Map, + yieldSupplyPromoBannerKey: String?, + onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?, + onYieldPromoCloseClick: (() -> Unit)?, + ): TokenItemState.PromoBannerState { + val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty + if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() || + yieldModuleApyMap[token.yieldSupplyKey()] == null + ) { + return TokenItemState.PromoBannerState.Empty + } + val yieldSupplyApy = + yieldModuleApyMap[token.yieldSupplyKey()] ?: return TokenItemState.PromoBannerState.Empty + + return TokenItemState.PromoBannerState.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(yieldSupplyApy), + ), + onPromoBannerClick = { + onApyLabelClick?.invoke(status, ApySource.YIELD_SUPPLY, yieldSupplyApy.toString()) + }, + onCloseClick = { + onYieldPromoCloseClick?.invoke() + }, + ) + } + private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange 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 ee0c984d38..037f71a2a6 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 @@ -122,6 +122,10 @@ object PreferencesKeys { val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") } + val YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY by lazy { + booleanPreferencesKey(name = "yieldSupplyShouldShowMainPromo") + } + val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") } // region Notifications diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 374b6de2e3..fc56519c01 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.currency.icon.CurrencyIcon @@ -28,6 +30,7 @@ import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State +import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -44,7 +47,7 @@ private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32 private enum class LayoutId { - ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT, PROMO_BANNER } /** @@ -109,6 +112,14 @@ fun TokenItem( .testTag(TokenElementsTestTags.TOKEN_ICON), ) + YieldSupplyPromoBanner( + state = state.promoBannerState, + modifier = Modifier + .layoutId(layoutId = LayoutId.PROMO_BANNER) + .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .fillMaxWidth(), + ) + TokenTitle( state = state.titleState, modifier = Modifier @@ -220,6 +231,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints) + val promoBanner = when (state.promoBannerState) { + is PromoBannerState.Content -> measurables.measure( + layoutId = LayoutId.PROMO_BANNER, + constraints = constraints, + ) + else -> null + } var firstRowRemainingFreeSpace: Int? = null var secondRowRemainingFreeSpace: Int? = null @@ -283,10 +301,18 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) } + val promoBannerHeight = promoBanner?.height ?: 0 + val promoOffset = if (promoBannerHeight > 0) { + promoBannerHeight - 8.dp.roundToPx() + } else { + 0 + } + val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, layoutPadding = verticalPadding, + promoOffset = promoOffset, title = title, fiatAmount = fiatAmount, cryptoAmount = cryptoAmount, @@ -294,16 +320,22 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2)) + promoBanner?.placeRelative(x = 0, y = 0) + + icon.placeRelative( + x = 0, + y = promoOffset + (layoutHeight - promoOffset - icon.height) + .div(other = 2), + ) title.placeRelative( x = icon.width, - y = when (state) { + y = promoOffset + when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { if (state.subtitleState == null) { - (layoutHeight - title.height).div(other = 2) + (layoutHeight - promoOffset - title.height).div(other = 2) } else { verticalPadding } @@ -314,8 +346,8 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier fiatAmount?.placeRelative( x = layoutWidth - fiatAmount.width, - y = when (state.subtitle2State) { - null -> (layoutHeight - fiatAmount.height).div(other = 2) + y = promoOffset + when (state.subtitle2State) { + null -> (layoutHeight - promoOffset - fiatAmount.height).div(other = 2) else -> verticalPadding }, ) @@ -335,7 +367,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier nonFiatContent.placeRelative( x = layoutWidth - nonFiatContent.width, - y = (layoutHeight - nonFiatContent.height).div(other = 2), + y = promoOffset + (layoutHeight - promoOffset - nonFiatContent.height).div(other = 2), ) } } @@ -443,6 +475,7 @@ private fun calculateLayoutHeight( state: TokenItemState, minLayoutHeight: Int, layoutPadding: Int, + promoOffset: Int, title: Placeable, fiatAmount: Placeable?, cryptoAmount: Placeable?, @@ -468,7 +501,7 @@ private fun calculateLayoutHeight( } } - return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight) + return (promoOffset + max(firstColumnHeight, secondColumnHeight)).coerceAtLeast(promoOffset + minLayoutHeight) } @Preview(widthDp = 360, showBackground = true) @@ -587,6 +620,11 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider YieldSupplyPromoBanner(state = state, modifier = modifier) + is PromoBannerState.Empty -> Unit + } +} + +@Composable +internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: Modifier = Modifier) { + val bgColor = TangemTheme.colors.control.unchecked + Column(modifier = modifier) { + Row( + modifier = Modifier + .background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 8.dp) + .clickable(onClick = state.onPromoBannerClick) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size16), + ) + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .weight(1f) + .padding(end = TangemTheme.dimens.spacing8), + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.secondary, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = { state.onCloseClick() }, + ), + ) + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rectangle_bottom), + contentDescription = null, + tint = bgColor, + modifier = Modifier + .size(width = 12.dp, height = 8.dp), + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_YieldSupplyPromoBanner() { + TangemThemePreview { + YieldSupplyPromoBanner( + state = PromoBannerState.Content( + title = TextReference.Str(value = "Earn up to 5% APY"), + onPromoBannerClick = {}, + onCloseClick = {}, + ), + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 7f05cdb7cb..a2180bb614 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -34,6 +34,8 @@ sealed class TokenItemState { */ abstract val subtitle2State: Subtitle2State? + abstract val promoBannerState: PromoBannerState + /** Callback which will be called when an item is clicked */ abstract val onItemClick: ((TokenItemState) -> Unit)? @@ -59,6 +61,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val subtitle2State: Subtitle2State = Subtitle2State.Loading + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -75,6 +78,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val subtitle2State: Subtitle2State = Subtitle2State.Locked + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -99,6 +103,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState?, override val subtitle2State: Subtitle2State?, + override val promoBannerState: PromoBannerState = PromoBannerState.Empty, override val onItemClick: ((TokenItemState) -> Unit)?, override val onItemLongClick: ((TokenItemState) -> Unit)?, override val onApyLabelClick: ((TokenItemState) -> Unit)? = null, @@ -120,6 +125,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -146,6 +152,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val subtitle2State: Subtitle2State? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } /** @@ -168,6 +175,7 @@ sealed class TokenItemState { override val subtitle2State: Subtitle2State? = null override val onItemClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } @Immutable @@ -254,4 +262,15 @@ sealed class TokenItemState { data object Locked : Subtitle2State() } + + @Immutable + sealed class PromoBannerState { + data class Content( + val title: TextReference, + val onPromoBannerClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : PromoBannerState() + + data object Empty : PromoBannerState() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 53ec6c23b8..884b8bfdae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -19,10 +19,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility +import androidx.constraintlayout.compose.atLeast import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -94,7 +96,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod bottom.linkTo(subtitleItem.top) start.linkTo(iconItem.end) end.linkTo(amountItem.start) - width = Dimension.fillToConstraints + width = Dimension.fillToConstraints.atLeast(50.dp) }, ) @@ -122,7 +124,6 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod visibility = state.isGoneIf { amount.isEmpty() } top.linkTo(parent.top) bottom.linkTo(timestampItem.top) - start.linkTo(titleItem.end) end.linkTo(parent.end) width = Dimension.fillToConstraints }, @@ -436,7 +437,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< ), TransactionState.Content( txHash = UUID.randomUUID().toString(), - amount = "0.625 USDT", + amount = "0.62521313 USDT", time = "€0.50", status = Status.Confirmed, direction = Direction.OUTGOING, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt index a6c08721f7..d513f7791c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt @@ -7,4 +7,5 @@ object TokenElementsTestTags { const val TOKEN_FIAT_AMOUNT = "TOKEN_FIAT_AMOUNT" const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT" const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK" + const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_connect_24.xml b/core/ui/src/main/res/drawable/ic_connect_24.xml new file mode 100644 index 0000000000..ebab5fcf69 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_connect_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_disconnect_24.xml b/core/ui/src/main/res/drawable/ic_disconnect_24.xml new file mode 100644 index 0000000000..2baf3ca1c9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_disconnect_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_gear_24.xml b/core/ui/src/main/res/drawable/ic_gear_24.xml new file mode 100644 index 0000000000..ea7f21abb2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_gear_24.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml new file mode 100644 index 0000000000..6b77c79cb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 8a2fd97b72..7d14d15efb 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -69,15 +69,16 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } @Suppress("LongParameterList", "MagicNumber") -inline fun combine6( +inline fun combine7( flow1: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, flow6: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, -): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + flow7: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6, flow7) { arr -> @Suppress("UNCHECKED_CAST") transform( arr[0] as T1, @@ -86,5 +87,6 @@ inline fun combine6( arr[3] as T4, arr[4] as T5, arr[5] as T6, + arr[6] as T7, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index b0e9c01c6e..9b04683948 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -148,14 +148,12 @@ internal object WalletConnectDataModule { @SdkMoshi moshi: Moshi, sessionsManager: WcSessionsManager, factories: WcEthNetwork.Factories, - walletManagersFacade: WalletManagersFacade, wcNetworksConverter: WcNetworksConverter, ): WcEthNetwork = WcEthNetwork( moshi = moshi, networksConverter = wcNetworksConverter, sessionsManager = sessionsManager, factories = factories, - walletManagersFacade = walletManagersFacade, ) @Provides @@ -165,24 +163,20 @@ internal object WalletConnectDataModule { wcNetworksConverter: WcNetworksConverter, sessionsManager: WcSessionsManager, factories: WcSolanaNetwork.Factories, - walletManagersFacade: WalletManagersFacade, ): WcSolanaNetwork = WcSolanaNetwork( moshi = moshi, sessionsManager = sessionsManager, factories = factories, networksConverter = wcNetworksConverter, - walletManagersFacade = walletManagersFacade, ) @Provides @Singleton fun caipNamespaceDelegate( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, - walletManagersFacade: WalletManagersFacade, wcNetworksConverter: WcNetworksConverter, ): CaipNamespaceDelegate = CaipNamespaceDelegate( namespaceConverters = namespaceConverters, - walletManagersFacade = walletManagersFacade, wcNetworksConverter = wcNetworksConverter, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index a14460a566..b713e9f18b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -18,7 +18,6 @@ import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import jakarta.inject.Inject internal class WcEthNetwork( @@ -26,7 +25,6 @@ internal class WcEthNetwork( private val sessionsManager: WcSessionsManager, private val factories: Factories, private val networksConverter: WcNetworksConverter, - private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? { @@ -57,7 +55,7 @@ internal class WcEthNetwork( is WcEthMethod.SwitchEthereumChain, -> anyExistNetwork() - ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() } val walletNetwork = when (method) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index ebaf668ba0..db6f83849d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -22,7 +22,6 @@ import com.tangem.domain.walletconnect.model.WcSolanaMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import jakarta.inject.Inject internal class WcSolanaNetwork( @@ -30,7 +29,6 @@ internal class WcSolanaNetwork( private val sessionsManager: WcSessionsManager, private val factories: Factories, private val networksConverter: WcNetworksConverter, - private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? { @@ -52,7 +50,7 @@ internal class WcSolanaNetwork( val chainId = request.chainId.orEmpty() suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) suspend fun anyAddress() = anyExistNetwork() - ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() val accountAddress = when (method) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt index 4b5d2c336a..3013709c9f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -9,11 +9,9 @@ import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.model.WcSessionApprove -import com.tangem.domain.walletmanager.WalletManagersFacade internal class CaipNamespaceDelegate( private val namespaceConverters: Set, - private val walletManagersFacade: WalletManagersFacade, private val wcNetworksConverter: WcNetworksConverter, ) { @@ -36,7 +34,7 @@ internal class CaipNamespaceDelegate( } suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? { - val address = walletManagersFacade.getDefaultAddress(userWalletId, network) + val address = wcNetworksConverter.getAddressForWC(userWalletId, network) val chainId = allWcNetworks .find { (wcNetwork, _) -> network.rawId == wcNetwork.rawId } ?.second diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 880b142dbd..a494eb772e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -1,6 +1,9 @@ package com.tangem.data.walletconnect.utils import com.reown.walletkit.client.Wallet +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.domain.account.producer.SingleAccountProducer @@ -44,7 +47,7 @@ internal class WcNetworksConverter @Inject constructor( val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet) val requestNetwork = allCoinNetwork.find { network -> - val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network) + val address = getAddressForWC(wallet.walletId, network) requestAddress.lowercase() == address?.lowercase() } return requestNetwork @@ -60,7 +63,18 @@ internal class WcNetworksConverter @Inject constructor( suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List { return filterWalletNetworkForRequest(rawChainId, wallet) - .mapNotNull { walletManagersFacade.getDefaultAddress(wallet.walletId, it)?.lowercase() } + .mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() } + } + + suspend fun getAddressForWC(userWalletId: UserWalletId, network: Network): String? { + return when (network.toBlockchain()) { + Blockchain.XDC, + Blockchain.XDCTestnet, + -> walletManagersFacade.getAddresses(userWalletId, network) + .find { address -> address.type == AddressType.Legacy } + ?.value + else -> walletManagersFacade.getDefaultAddress(userWalletId, network) + } } /** @@ -94,8 +108,8 @@ internal class WcNetworksConverter @Inject constructor( // find all derivation .filter { it.rawId == blockchain.id } // find equal address - .firstOrNull { - val walletAddress = walletManagersFacade.getDefaultAddress(wallet.walletId, it) + .firstOrNull { network -> + val walletAddress = getAddressForWC(wallet.walletId, network) walletAddress?.lowercase() == caip10.accountAddress.lowercase() } } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 40e874e14e..1a677b9cdb 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -24,7 +24,7 @@ internal class SdkTransactionHistoryItemConverter( SdkTransactionHistoryItem.TransactionStatus.Failed -> TxInfo.TransactionStatus.Failed SdkTransactionHistoryItem.TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed }, - type = typeConverter.convert(value.type), + type = typeConverter.convert(value.type to value.destinationType.toDomain()), amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" }, ) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt index 7e674c1f59..5e9fe1dc0b 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -1,21 +1,27 @@ package com.tangem.data.walletmanager.utils import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyInitTokenCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyReactivateTokenCallData import com.tangem.domain.models.network.TxInfo import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter internal class SdkTransactionTypeConverter( private val smartContractMethods: Map, -) : Converter { +) : Converter, TxInfo.TransactionType> { - override fun convert(value: TransactionType): TxInfo.TransactionType { - return when (value) { + override fun convert(value: Pair): TxInfo.TransactionType { + val (type, destination) = value + + return when (type) { is TransactionType.ContractMethod -> { - getTransactionType(methodName = smartContractMethods[value.id]?.name) + getTransactionType(methodName = smartContractMethods[type.id]?.name, type.callData, destination) } is TransactionType.ContractMethodName -> { - getTransactionType(methodName = value.name) + getTransactionType(methodName = type.name, type.callData, destination) } is TransactionType.Transfer -> { TxInfo.TransactionType.Transfer @@ -27,7 +33,7 @@ internal class SdkTransactionTypeConverter( TxInfo.TransactionType.Staking.Unstake } is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { - TxInfo.TransactionType.Staking.Vote(value.validatorAddress) + TxInfo.TransactionType.Staking.Vote(type.validatorAddress) } is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { TxInfo.TransactionType.Staking.ClaimRewards @@ -38,7 +44,12 @@ internal class SdkTransactionTypeConverter( } } - private fun getTransactionType(methodName: String?): TxInfo.TransactionType { + @Suppress("CyclomaticComplexMethod") + private fun getTransactionType( + methodName: String?, + callData: String?, + destination: TxInfo.DestinationType, + ): TxInfo.TransactionType { return when (methodName) { "transfer" -> TxInfo.TransactionType.Transfer "approve" -> TxInfo.TransactionType.Approve @@ -59,11 +70,33 @@ internal class SdkTransactionTypeConverter( "withdrawRewardsPOL", -> TxInfo.TransactionType.Staking.ClaimRewards "redelegate" -> TxInfo.TransactionType.Staking.Restake - "supplyEnter" -> TxInfo.TransactionType.YieldSupply.Enter - "supplyExit" -> TxInfo.TransactionType.YieldSupply.Exit + "yieldSend" -> TxInfo.TransactionType.YieldSupply.Send + "enterProtocolByOwner" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.Enter( + EthereumYieldSupplyEnterCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "withdrawAndDeactivate" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.Exit( + EthereumYieldSupplyExitCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "deployYieldModule" -> TxInfo.TransactionType.YieldSupply.DeployContract( + (destination as? TxInfo.DestinationType.Single)?.addressType?.address.orEmpty(), + ) + "initYieldToken" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.InitializeToken( + EthereumYieldSupplyInitTokenCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "reactivateToken" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.ReactivateToken( + EthereumYieldSupplyReactivateTokenCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } "supplyTopUp" -> TxInfo.TransactionType.YieldSupply.Topup null -> TxInfo.TransactionType.UnknownOperation else -> TxInfo.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) - } + } ?: TxInfo.TransactionType.Operation(name = methodName?.replaceFirstChar { it.titlecase() }.orEmpty()) } } \ No newline at end of file diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index dae65d3ec7..36016b121b 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -45,7 +45,7 @@ internal class TransactionDataToTxHistoryItemConverter( TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed }, - type = getTransactionType(value.extras), + type = getTransactionType(value), amount = amount, ) } @@ -99,16 +99,25 @@ internal class TransactionDataToTxHistoryItemConverter( ) } - private fun getTransactionType(extras: TransactionExtras?): TxInfo.TransactionType { - return when (extras) { + private fun getTransactionType(transactionData: TransactionData.Uncompiled?): TxInfo.TransactionType { + return when (val extras = transactionData?.extras) { is EthereumTransactionExtras -> { - when (extras.callData) { - is EthereumYieldSupplyDeployCallData, - is EthereumYieldSupplyReactivateTokenCallData, - is EthereumYieldSupplyInitTokenCallData, - is EthereumYieldSupplyEnterCallData, - -> TxInfo.TransactionType.YieldSupply.Enter - is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit + when (val callData = extras.callData) { + is EthereumYieldSupplyDeployCallData -> TxInfo.TransactionType.YieldSupply.DeployContract( + transactionData.destinationAddress, + ) + is EthereumYieldSupplyReactivateTokenCallData -> TxInfo.TransactionType.YieldSupply.ReactivateToken( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyInitTokenCallData -> TxInfo.TransactionType.YieldSupply.InitializeToken( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyEnterCallData -> TxInfo.TransactionType.YieldSupply.Enter( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit( + callData.tokenContractAddress, + ) is ApprovalERC20TokenCallData -> TxInfo.TransactionType.Approve else -> TxInfo.TransactionType.Transfer } diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index 4fbb4f314a..cc7313cd67 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -19,6 +19,10 @@ dependencies { /** Tangem SDKs */ implementation(tangemDeps.blockchain) + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + /** Core */ implementation(projects.core.datasource) implementation(projects.core.utils) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index e652292644..a40499ba87 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -12,6 +12,10 @@ import com.tangem.data.yield.supply.converters.YieldTokenChartConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -35,6 +39,7 @@ internal class DefaultYieldSupplyRepository( private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val appPreferencesStore: AppPreferencesStore, ) : YieldSupplyRepository { private val statusMap: MutableMap = ConcurrentHashMap() @@ -174,6 +179,14 @@ internal class DefaultYieldSupplyRepository( null } + override fun getShouldShowYieldPromoBanner(): Flow { + return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true) + } + + override suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) { + appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, shouldShow) + } + private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { it.type == TxInfo.TransactionType.YieldSupply.Enter || it.type == TxInfo.TransactionType.Approve && diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 409d1da57f..ee040a543a 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.datasource.api.tangemTech.YieldSupplyApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyRepository @@ -41,6 +42,7 @@ internal object YieldSupplyDataModule { walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, analyticsExceptionHandler: AnalyticsExceptionHandler, + appPreferencesStore: AppPreferencesStore, ): YieldSupplyRepository { return DefaultYieldSupplyRepository( yieldSupplyApi = yieldSupplyApi, @@ -48,6 +50,7 @@ internal object YieldSupplyDataModule { dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, analyticsExceptionHandler = analyticsExceptionHandler, + appPreferencesStore = appPreferencesStore, ) } diff --git a/domain/legacy/src/main/assets/contract_methods.json b/domain/legacy/src/main/assets/contract_methods.json index 5ca94cd491..fe538d3526 100644 --- a/domain/legacy/src/main/assets/contract_methods.json +++ b/domain/legacy/src/main/assets/contract_methods.json @@ -205,32 +205,32 @@ "0xcbeda14c": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "deployYieldModule" }, "0x79be55f7": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supplyEnter" + "name": "enterProtocolByOwner" }, "0xc65e6dcf": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supplyExit" + "name": "withdrawAndDeactivate" }, "0xebd4b81c": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "initYieldToken" }, "0xc478e956": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "reactivateToken" }, "0x0779afe6": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "transfer" + "name": "yieldSend" }, "0xb9de6a93": { "info": "yieldModule", diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index 4937c3c0af..09c43362de 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -11,9 +11,9 @@ fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean { return notSupplied > BigDecimal.ZERO } -fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount: BigDecimal): Boolean { +fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount: BigDecimal): Boolean { val notSupplied = notSuppliedAmountOrNull() ?: return false - return notSupplied >= minAmount + return notSupplied >= dustAmount } fun CryptoCurrencyStatus.notSuppliedAmountOrNull(): BigDecimal? { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index 65467f7460..d8fd074625 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -110,14 +110,32 @@ data class TxInfo( @Serializable sealed interface YieldSupply : TransactionType { - @Serializable - data object Enter : YieldSupply + val address: String? @Serializable - data object Exit : YieldSupply + data class Enter(override val address: String) : YieldSupply @Serializable - data object Topup : YieldSupply + data class Exit(override val address: String) : YieldSupply + + @Serializable + data object Topup : YieldSupply { + override val address: String? = null + } + + @Serializable + data object Send : YieldSupply { + override val address: String? = null + } + + @Serializable + data class DeployContract(override val address: String) : YieldSupply + + @Serializable + data class ReactivateToken(override val address: String) : YieldSupply + + @Serializable + data class InitializeToken(override val address: String) : YieldSupply } @Serializable diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt new file mode 100644 index 0000000000..d3086742cb --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.yield.supply.models + +data class YieldSupplyRewardBalance( + val fiatBalance: String?, + val cryptoBalance: String?, +) { + companion object { + fun empty() = YieldSupplyRewardBalance(null, null) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 8ab13bbac1..9a73322ca8 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -108,4 +108,8 @@ interface YieldSupplyRepository { userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): YieldSupplyEnterStatus? + + fun getShouldShowYieldPromoBanner(): Flow + + suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index d93fcca1a1..3dda098013 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.math.BigDecimal /** * Emits a map of APY values per token. @@ -15,11 +16,11 @@ class YieldSupplyApyFlowUseCase( private val yieldSupplyRepository: YieldSupplyRepository, ) { - operator fun invoke(): Flow> { + operator fun invoke(): Flow> { return yieldSupplyRepository.getMarketsFlow() .map { yieldMarketTokenList -> yieldMarketTokenList.filter { it.isActive }.associate { token -> - token.yieldSupplyKey to token.apy.toString() + token.yieldSupplyKey to token.apy } } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt new file mode 100644 index 0000000000..7814d60add --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +class YieldSupplyGetDustMinAmountUseCase { + + operator fun invoke(minAmount: BigDecimal, appCurrency: AppCurrency): BigDecimal { + return if (appCurrency.code in SUPPORTED_DUST_CURRENCIES) { + DUST_MIN_AMOUNT + } else { + minAmount.stripTrailingZeros() + } + } + + companion object { + private val DUST_MIN_AMOUNT = BigDecimal("0.1") + private val SUPPORTED_DUST_CURRENCIES = setOf("EUR", "USD", "AUD", "CAD", "GBP") + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index 5e1b8676a9..93e18928bf 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -1,11 +1,13 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -22,7 +24,7 @@ class YieldSupplyGetRewardsBalanceUseCase( private val dispatcherProvider: CoroutineDispatcherProvider, ) { - operator fun invoke(status: CryptoCurrencyStatus, appCurrency: AppCurrency): Flow = flow { + operator fun invoke(status: CryptoCurrencyStatus, appCurrency: AppCurrency): Flow = flow { val cryptoAmount = status.value.amount val fiatRate = status.value.fiatRate @@ -30,11 +32,7 @@ class YieldSupplyGetRewardsBalanceUseCase( return@flow } - val amount = if (cryptoAmount != null && fiatRate != null) { - cryptoAmount.multiply(fiatRate) - } else { - return@flow - } + if (cryptoAmount == null) return@flow val tokenAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress ?: return@flow val apy = try { @@ -50,37 +48,57 @@ class YieldSupplyGetRewardsBalanceUseCase( return@flow } - val initialPerTickDelta = amount - .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) - .abs() + val initialPerTickDeltaCrypto = perTickDelta(cryptoAmount, apyFraction).abs() - val minVisibleDecimals = calculateMinVisibleDecimals(initialPerTickDelta) + val minVisibleDecimalsCrypto = calculateMinVisibleDecimals( + perTickDeltaAbs = initialPerTickDeltaCrypto, + maxDecimals = status.currency.decimals, + ) - var currentBalance: BigDecimal = amount + val fiatAmountStart = fiatRate?.let { cryptoAmount.multiply(it) } + val minVisibleDecimalsFiat = fiatAmountStart?.let { amount -> + val initialPerTickDeltaFiat = perTickDelta(amount, apyFraction).abs() + calculateMinVisibleDecimals( + perTickDeltaAbs = initialPerTickDeltaFiat, + maxDecimals = FIAT_MAX_DECIMALS, + ) + } + + var currentCryptoBalance: BigDecimal = cryptoAmount + var currentFiatBalance: BigDecimal? = fiatAmountStart while (true) { + val fiatBalanceFormatted: String? = currentFiatBalance?.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).anyDecimals(decimals = minVisibleDecimalsFiat ?: FIAT_MIN_DECIMALS) + } + + val cryptoBalanceFormatted: String = currentCryptoBalance.format { + crypto(status.currency).anyDecimals( + maxDecimals = minVisibleDecimalsCrypto, + minDecimals = minVisibleDecimalsCrypto, + ) + } + emit( - currentBalance.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ).anyDecimals(decimals = minVisibleDecimals) - }, + YieldSupplyRewardBalance(fiatBalance = fiatBalanceFormatted, cryptoBalance = cryptoBalanceFormatted), ) - val perTickDelta = currentBalance - .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + val perTickDeltaCrypto = perTickDelta(currentCryptoBalance, apyFraction) - currentBalance = currentBalance.add(perTickDelta) + currentCryptoBalance = currentCryptoBalance.add(perTickDeltaCrypto) + + currentFiatBalance = currentFiatBalance?.let { current -> + val perTickDeltaFiat = perTickDelta(current, apyFraction) + current.add(perTickDeltaFiat) + } delay(TICK_MILLIS) } }.flowOn(dispatcherProvider.default) - private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal): Int { + private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal, maxDecimals: Int): Int { if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS val perTickAsDouble = perTickDeltaAbs.toDouble() @@ -88,20 +106,28 @@ class YieldSupplyGetRewardsBalanceUseCase( val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble val raw = ceil(-ln(safe) / LN_10) - return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS) + return raw.toInt().coerceIn(MIN_DECIMALS, maxDecimals) } - private companion object { - const val TICK_MILLIS: Long = 300 - private val TICK_SECONDS_BD = BigDecimal("0.3") - private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") // 365 * 24 * 60 * 60 - private val HUNDRED_BD = BigDecimal("100") - private const val SCALE = 18 + private fun perTickDelta(amount: BigDecimal, apyFraction: BigDecimal): BigDecimal { + return amount + .multiply(apyFraction) + .multiply(TICK_SECONDS_BD) + .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + } - private const val MIN_DECIMALS = 3 - private const val MAX_DECIMALS = 12 + companion object { + internal const val TICK_MILLIS: Long = 800 + internal val TICK_SECONDS_BD: BigDecimal = BigDecimal("0.8") + internal val SECONDS_PER_YEAR_BD: BigDecimal = BigDecimal("31536000") // 365 * 24 * 60 * 60 + internal val HUNDRED_BD: BigDecimal = BigDecimal("100") + internal const val SCALE: Int = 18 - private val LN_10 = ln(10.0) - private const val EPSILON = 1e-18 + internal const val MIN_DECIMALS: Int = 3 + internal const val FIAT_MIN_DECIMALS: Int = 2 + internal const val FIAT_MAX_DECIMALS: Int = 12 + + internal val LN_10: Double = ln(10.0) + internal const val EPSILON: Double = 1e-18 } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..42a1493d5b --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository +import kotlinx.coroutines.flow.Flow + +class YieldSupplyGetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + operator fun invoke(): Flow { + return yieldSupplyRepository.getShouldShowYieldPromoBanner() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..dcc0a985e6 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplySetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(shouldShow: Boolean) { + yieldSupplyRepository.setShouldShowYieldPromoBanner(shouldShow) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt new file mode 100644 index 0000000000..50a00ac69c --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.yield.supply.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.appcurrency.model.AppCurrency +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class YieldSupplyGetDustMinAmountUseCaseTest { + + private val useCase = YieldSupplyGetDustMinAmountUseCase() + + @Test + fun `GIVEN supported currency WHEN invoke THEN return dust min amount`() { + val minAmount = BigDecimal("123.456") + val appCurrency = AppCurrency(code = "EUR", name = "Euro", symbol = "€") + + val result = useCase(minAmount, appCurrency) + + assertThat(result).isEqualTo(BigDecimal("0.1")) + } + + @Test + fun `GIVEN unsupported currency WHEN invoke THEN return min amount stripped`() { + val minAmount = BigDecimal("1.2300") + val appCurrency = AppCurrency(code = "JPY", name = "Japanese Yen", symbol = "¥") + + val result = useCase(minAmount, appCurrency) + + assertThat(result).isEqualTo(BigDecimal("1.23")) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index 254c8c6d4a..c086813f73 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -11,6 +12,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase.Companion.TICK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.mockk @@ -26,7 +28,6 @@ import org.junit.jupiter.api.Test import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.ceil -import kotlin.math.ln class YieldSupplyGetRewardsBalanceUseCaseTest { @@ -165,9 +166,9 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { val deferred = async { useCase(status, appCurrency).take(3).toList() } testScheduler.advanceUntilIdle() - advanceTimeBy(300) + advanceTimeBy(TICK_MILLIS) testScheduler.advanceUntilIdle() - advanceTimeBy(300) + advanceTimeBy(TICK_MILLIS) testScheduler.advanceUntilIdle() val collected = deferred.await() @@ -175,25 +176,147 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { val expectedDecimals = calculateMinVisibleDecimalsForTest(initialPerTickDelta(amount, apy)) - val firstExpected = amount.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[0]).isEqualTo(firstExpected) + val firstExpected = amount.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[0].fiatBalance).isEqualTo(firstExpected) val firstNext = nextBalance(amount, apy) - val secondExpected = firstNext.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[1]).isEqualTo(secondExpected) + val secondExpected = firstNext.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[1].fiatBalance).isEqualTo(secondExpected) val secondNext = nextBalance(firstNext, apy) - val thirdExpected = secondNext.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[2]).isEqualTo(thirdExpected) + val thirdExpected = secondNext.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[2].fiatBalance).isEqualTo(thirdExpected) + } + + @Test + fun `GIVEN polygon USDT0 Loaded status WHEN invoke THEN emit formatted balances`() = runTest { + val network = Network( + id = Network.ID(Network.RawID("POLYGON"), Network.DerivationPath.Card("m/44'/60'/0'/0/0")), + backendId = "polygon-pos", + name = "Polygon", + currencySymbol = "POL", + derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0"), + isTestnet = false, + standardType = Network.StandardType.Unspecified("Polygon"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + val tokenId = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(network.rawId), + suffix = CryptoCurrency.ID.Suffix.RawID("usdt0", "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"), + ) + val currency = CryptoCurrency.Token( + id = tokenId, + network = network, + name = "USDT0", + symbol = "USDT0", + decimals = 6, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/usdt0.png", + isCustom = false, + contractAddress = "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + ) + + val amount = BigDecimal("9.241136") + val fiatRate = BigDecimal("0.9999761277273864") + val status = CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = amount.multiply(fiatRate), + fiatRate = fiatRate, + priceChange = BigDecimal("-0.000058200000000008245"), + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address( + value = "0xb71fa0E20ba8579B3ec51cC79aaa84Bf5982BB49", + type = NetworkAddress.Address.Type.Primary, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + val apy = BigDecimal("5.0") + coEvery { repository.getCachedMarkets() } returns listOf( + YieldMarketToken( + tokenAddress = currency.contractAddress, + chainId = 137, + apy = apy, + isActive = true, + maxFeeNative = BigDecimal.ZERO, + maxFeeUSD = BigDecimal.ZERO, + backendId = "polygon-pos", + ), + ) + + val dispatcherProvider = testDispatcherProvider(this) + val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider) + val appCurrency = AppCurrency.Default + + val deferred = async { useCase(status, appCurrency).take(2).toList() } + + testScheduler.advanceUntilIdle() + advanceTimeBy(TICK_MILLIS) + testScheduler.advanceUntilIdle() + + val emissions = deferred.await() + assertThat(emissions).hasSize(2) + + val apyFraction = apy.divide(BigDecimal("100"), 18, RoundingMode.HALF_UP) + val perTickCrypto = amount.multiply(apyFraction) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) + .abs() + val minCryptoDecimals = calculateMinVisibleDecimalsForTest(perTickCrypto).coerceAtMost(currency.decimals) + + val fiatAmountStart = amount.multiply(fiatRate) + val perTickFiat = fiatAmountStart.multiply(apyFraction) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) + .abs() + val minFiatDecimals = calculateMinVisibleDecimalsForTest(perTickFiat) + + val expectedCrypto0 = amount.format { + crypto(currency).anyDecimals( + maxDecimals = minCryptoDecimals, + minDecimals = minCryptoDecimals, + ) + } + val expectedFiat0 = fiatAmountStart.format { + fiat(appCurrency.code, appCurrency.symbol).anyDecimals(decimals = minFiatDecimals) + } + + assertThat(emissions[0].cryptoBalance).isEqualTo(expectedCrypto0) + assertThat(emissions[0].fiatBalance).isEqualTo(expectedFiat0) } private fun testDispatcherProvider(scope: TestScope): CoroutineDispatcherProvider { @@ -260,42 +383,48 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { } private fun initialPerTickDelta(amount: BigDecimal, apy: BigDecimal): BigDecimal { - val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP) + val apyFraction = apy.divide( + YieldSupplyGetRewardsBalanceUseCase.HUNDRED_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) return amount .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) .abs() } private fun nextBalance(current: BigDecimal, apy: BigDecimal): BigDecimal { - val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP) + val apyFraction = apy.divide( + YieldSupplyGetRewardsBalanceUseCase.HUNDRED_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) val perTickDelta = current .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) return current.add(perTickDelta) } private fun calculateMinVisibleDecimalsForTest(perTickDeltaAbs: BigDecimal): Int { - if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS + if (perTickDeltaAbs <= BigDecimal.ZERO) return YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS val perTickAsDouble = perTickDeltaAbs.toDouble() - if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return MIN_DECIMALS - val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble - val raw = ceil(-ln(safe) / LN_10) - return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS) - } - - private companion object { - private const val SCALE = 18 - private val TICK_SECONDS_BD = BigDecimal("0.3") - private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") - private val HUNDRED_BD = BigDecimal("100") - - private const val MIN_DECIMALS = 3 - private const val MAX_DECIMALS = 8 - - private val LN_10 = ln(10.0) - private const val EPSILON = 1e-18 + if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS + val safe = if (perTickAsDouble <= 0.0) YieldSupplyGetRewardsBalanceUseCase.EPSILON else perTickAsDouble + val raw = ceil(-kotlin.math.ln(safe) / YieldSupplyGetRewardsBalanceUseCase.LN_10) + return raw.toInt().coerceIn( + YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS, + YieldSupplyGetRewardsBalanceUseCase.FIAT_MAX_DECIMALS, + ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 2d9665f967..05ec40ad40 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.yieldSupply) + implementation(projects.domain.yieldSupply.models) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 4e70a2faa4..a221f254ec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -121,7 +121,7 @@ internal object TokenDetailsPreviewData { selectedBalanceType = BalanceType.ALL, onBalanceSelect = {}, displayCryptoBalance = "966,96 XLM", - displayYeildSupplyCryptoBalance = null, + displayYieldSupplyFiatBalance = null, displayFiatBalance = "91,50$", isBalanceSelectorEnabled = true, isBalanceFlickering = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 809fa4ea3f..16ce0aecda 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -75,6 +75,7 @@ import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter @@ -416,7 +417,9 @@ internal class TokenDetailsModel @Inject constructor( .saveIn(yieldSupplyBalanceJobHolder) } else { yieldSupplyBalanceJobHolder.cancel() - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(null) + internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( + YieldSupplyRewardBalance.empty(), + ) } } @@ -1251,14 +1254,8 @@ internal class TokenDetailsModel @Inject constructor( } private fun handleNavigationParam() { - when (val action = params.navigationAction) { - is NavigationAction.Staking -> openStaking() - is NavigationAction.YieldSupply -> if (action.isActive) { - modelScope.launch(dispatchers.default) { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id) - } - } - else -> Unit + if (params.navigationAction is NavigationAction.Staking) { + openStaking() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index a74d80f6b8..9e17b95c95 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -28,7 +28,8 @@ internal sealed class TokenDetailsBalanceBlockState { val isBalanceSelectorEnabled: Boolean, val isBalanceFlickering: Boolean, val yieldSupplyState: TokenDetailsYieldSupplyState = TokenDetailsYieldSupplyState.Empty, - val displayYeildSupplyCryptoBalance: String? = null, + val displayYieldSupplyFiatBalance: String? = null, + val displayYieldSupplyCryptoBalance: String? = null, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 3243636297..306315e766 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -98,8 +98,8 @@ internal class TokenDetailsLoadedBalanceConverter( stakingCryptoAmount, currentState.selectedBalanceType, ), - displayYeildSupplyCryptoBalance = (currentState as? TokenDetailsBalanceBlockState.Content) - ?.displayYeildSupplyCryptoBalance, + displayYieldSupplyFiatBalance = (currentState as? TokenDetailsBalanceBlockState.Content) + ?.displayYieldSupplyFiatBalance, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, 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 e19dda0395..da13ce39ba 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 @@ -26,6 +26,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig @@ -315,13 +316,18 @@ internal class TokenDetailsStateFactory( return balanceSelectStateConverter.convert(buttonConfig) } - fun getStateWithUpdatedYieldSupplyDisplayBalance(displayBalance: String?): TokenDetailsState { + fun getStateWithUpdatedYieldSupplyDisplayBalance( + yieldSupplyRewardBalance: YieldSupplyRewardBalance, + ): TokenDetailsState { val state = currentStateProvider() val balanceState = state.tokenBalanceBlockState return state.copy( tokenBalanceBlockState = when (balanceState) { is TokenDetailsBalanceBlockState.Content -> - balanceState.copy(displayYeildSupplyCryptoBalance = displayBalance) + balanceState.copy( + displayYieldSupplyFiatBalance = yieldSupplyRewardBalance.fiatBalance, + displayYieldSupplyCryptoBalance = yieldSupplyRewardBalance.cryptoBalance, + ) is TokenDetailsBalanceBlockState.Error -> balanceState is TokenDetailsBalanceBlockState.Loading -> balanceState }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 9eaa517709..293e54545c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -122,12 +122,12 @@ private fun FiatBalance( height = TangemTheme.dimens.size32, ), ) - is TokenDetailsBalanceBlockState.Content -> if (state.displayYeildSupplyCryptoBalance != null && + is TokenDetailsBalanceBlockState.Content -> if (state.displayYieldSupplyFiatBalance != null && !isBalanceHidden ) { TextAnimatedCounter( modifier = modifier, - text = state.displayYeildSupplyCryptoBalance, + text = state.displayYieldSupplyFiatBalance, style = TangemTheme.typography.h2.applyBladeBrush( isEnabled = state.isBalanceFlickering, textColor = TangemTheme.colors.text.primary1, @@ -136,7 +136,7 @@ private fun FiatBalance( } else { Text( modifier = modifier, - text = (state.displayYeildSupplyCryptoBalance ?: state.displayFiatBalance).orMaskWithStars( + text = (state.displayYieldSupplyFiatBalance ?: state.displayFiatBalance).orMaskWithStars( isBalanceHidden, ), style = TangemTheme.typography.h2.applyBladeBrush( @@ -184,8 +184,9 @@ private fun CryptoBalance( tint = TangemTheme.colors.icon.inactive, contentDescription = null, ) - Text( - text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden), + TextAnimatedCounter( + text = (state.displayYieldSupplyCryptoBalance ?: state.displayCryptoBalance) + .orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2.applyBladeBrush( isEnabled = state.isBalanceFlickering, textColor = TangemTheme.colors.text.tertiary, diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt index 0fc75e0d67..bc5f160b47 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt @@ -41,7 +41,9 @@ internal class TxHistoryItemToTransactionStateConverter( R.drawable.ic_close_24 } else { when (type) { - is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.YieldSupply.DeployContract, + is TransactionType.Approve, + -> R.drawable.ic_doc_24 is TransactionType.Staking.Stake, is TransactionType.Staking.Vote, is TransactionType.Staking.Restake, @@ -51,11 +53,16 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.Staking.Unstake, is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 + is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24 + is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24 + is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24 + is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, - is TransactionType.YieldSupply, is TransactionType.UnknownOperation, + TransactionType.YieldSupply.Send, + TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } @@ -66,32 +73,76 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) - is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) - is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) + is TransactionType.YieldSupply -> when (type) { + is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) + is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) + TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) + TransactionType.YieldSupply.Send -> if (isOutgoing) { + resourceReference(R.string.common_transfer) + } else { + resourceReference(R.string.yield_module_transaction_withdraw) + } + is TransactionType.YieldSupply.DeployContract -> resourceReference( + R.string + .yield_module_transaction_deploy_contract, + ) + is TransactionType.YieldSupply.InitializeToken -> resourceReference( + R.string + .yield_module_transaction_initialize, + ) + is TransactionType.YieldSupply.ReactivateToken -> resourceReference( + R.string + .yield_module_transaction_reactivate, + ) + } is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } private fun TxInfo.extractSubtitle(): TextReference { - return when (this.type) { - is TransactionType.YieldSupply.Enter -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Topup, - -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Exit -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + return when (val type = this.type) { + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) { + if (type == TransactionType.YieldSupply.Send) { + extractSubtitleByAddressType() + } else { + resourceReference( + R.string.transaction_history_transaction_for_address, + wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + ) + } + } else { + when (type) { + is TransactionType.YieldSupply.Enter -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) + } + TransactionType.YieldSupply.Topup -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) + } + is TransactionType.YieldSupply.Exit -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + } + TransactionType.YieldSupply.Send -> { + if (isOutgoing) { + extractSubtitleByAddressType() + } else { + val amount = + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + } + else -> extractSubtitleByAddressType() + } } else -> extractSubtitleByAddressType() } @@ -133,14 +184,21 @@ internal class TxHistoryItemToTransactionStateConverter( @Suppress("ComplexCondition") private fun TxInfo.getAmount(): String { - if (type is TransactionType.Staking.Vote || - type == TransactionType.Staking.ClaimRewards || - type == TransactionType.Staking.Withdraw || - type == TransactionType.YieldSupply.Enter || - type == TransactionType.YieldSupply.Exit || - type == TransactionType.YieldSupply.Topup - ) { - return "" + when (type) { + is TransactionType.Staking.Vote, + TransactionType.Staking.ClaimRewards, + TransactionType.Staking.Withdraw, + -> return "" + + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Token) { + when (type) { + TransactionType.YieldSupply.Send -> if (!isOutgoing) { + return "" + } + else -> return "" + } + } + else -> Unit } val prefix = when { status == TxInfo.TransactionStatus.Failed -> "" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index b6d7d15a65..4c0038bfe2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -21,6 +21,7 @@ import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap @@ -57,6 +58,8 @@ internal interface WalletContentClickIntents { apy: String, ) + fun onYieldPromoCloseClick() + fun onAccountExpandClick(account: Account) fun onAccountCollapseClick(account: Account) @@ -94,6 +97,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val accountDependencies: AccountDependencies, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, + private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -193,6 +197,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } + override fun onYieldPromoCloseClick() { + modelScope.launch { + yieldSupplySetShouldShowMainPromoUseCase(false) + } + } + override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 0ce2d80a8d..e08e469afc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -45,6 +46,7 @@ internal class MultiWalletContentLoader( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -63,6 +65,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) WalletNFTListSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index d3fe3d60a4..1f9d0570e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -44,6 +45,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -72,6 +74,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 7f3e9f91b6..6d895697ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -6,6 +6,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -35,6 +36,7 @@ internal class SingleWalletWithTokenContentLoader( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -49,6 +51,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 0f6ac06ef5..4d89203c74 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -7,6 +7,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -36,6 +37,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -55,6 +57,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4d102bb5d8..0dec24b4b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -11,14 +11,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber +import java.math.BigDecimal internal class SetTokenListTransformer( private val params: TokenConverterParams, private val userWallet: UserWallet, private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, - private val yieldSupplyApyMap: Map = emptyMap(), + private val yieldSupplyApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val shouldShowMainPromo: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -63,6 +65,7 @@ internal class SetTokenListTransformer( clickIntents = clickIntents, yieldModuleApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index 3b5dfff91c..fa92bb7623 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.models.wallet.UserWallet @@ -17,6 +18,7 @@ internal class SetTxHistoryCountErrorTransformer( private val error: TxHistoryStateError, private val pendingTransactions: Set, private val clickIntents: WalletClickIntents, + private val currency: CryptoCurrency, ) : WalletStateTransformer(userWallet.walletId) { private val txHistoryItemConverter by lazy { @@ -26,6 +28,7 @@ internal class SetTxHistoryCountErrorTransformer( } TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 6dd7d7de6c..257e68fc6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -28,17 +28,25 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig +@Suppress("LongParameterList") internal class TokenListStateConverter( private val appCurrency: AppCurrency, private val params: TokenConverterParams, private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, - private val yieldModuleApyMap: Map, + private val yieldModuleApyMap: Map, private val stakingApyMap: Map>, + private val shouldShowMainPromo: Boolean, ) : Converter { + private val yieldSupplyPromoBannerKeyConverter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) @@ -63,10 +71,12 @@ internal class TokenListStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params), stakingApyMap = stakingApyMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, + onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, ) override fun convert(value: WalletTokensListState): WalletTokensListState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index db435626e8..a30612c67e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.network.TxInfo.TransactionStatus import com.tangem.domain.models.network.TxInfo.TransactionType @@ -20,6 +21,7 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat internal class TxHistoryItemStateConverter( + private val currency: CryptoCurrency, private val symbol: String, private val decimals: Int, private val clickIntents: WalletClickIntents, @@ -49,7 +51,9 @@ internal class TxHistoryItemStateConverter( R.drawable.ic_close_24 } else { when (type) { - is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.YieldSupply.DeployContract, + is TransactionType.Approve, + -> R.drawable.ic_doc_24 is TransactionType.Staking.Stake, is TransactionType.Staking.Vote, is TransactionType.Staking.Restake, @@ -59,11 +63,16 @@ internal class TxHistoryItemStateConverter( is TransactionType.Staking.Unstake, is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 + is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24 + is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24 + is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24 + is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, - is TransactionType.YieldSupply, is TransactionType.UnknownOperation, + TransactionType.YieldSupply.Send, + TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } @@ -74,32 +83,81 @@ internal class TxHistoryItemStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) - is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) - is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) + is TransactionType.YieldSupply -> when (type) { + is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) + is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) + TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) + TransactionType.YieldSupply.Send -> if (isOutgoing) { + resourceReference(R.string.common_transfer) + } else { + resourceReference(R.string.yield_module_transaction_withdraw) + } + is TransactionType.YieldSupply.DeployContract -> resourceReference( + R.string + .yield_module_transaction_deploy_contract, + ) + is TransactionType.YieldSupply.InitializeToken -> resourceReference( + R.string + .yield_module_transaction_initialize, + ) + is TransactionType.YieldSupply.ReactivateToken -> resourceReference( + R.string + .yield_module_transaction_reactivate, + ) + } is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } private fun TxInfo.extractSubtitle(): TextReference { - return when (this.type) { - is TransactionType.YieldSupply.Enter -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Topup, - -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Exit -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + return when (val type = this.type) { + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) { + resourceReference( + R.string.transaction_history_transaction_for_address, + wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + ) + } else { + when (type) { + is TransactionType.YieldSupply.Enter -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_enter_subtitle, + wrappedList(amount), + ) + } + TransactionType.YieldSupply.Topup -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_topup_subtitle, + wrappedList(amount), + ) + } + TransactionType.YieldSupply.Send -> { + if (isOutgoing) { + extractSubtitleByAddressType() + } else { + val amount = + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + } + is TransactionType.YieldSupply.Exit -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + else -> extractSubtitleByAddressType() + } } else -> extractSubtitleByAddressType() } @@ -147,14 +205,18 @@ internal class TxHistoryItemStateConverter( @Suppress("ComplexCondition") private fun TxInfo.getAmount(): String { - if (type is TransactionType.Staking.Vote || - type == TransactionType.Staking.ClaimRewards || - type == TransactionType.Staking.Withdraw || - type == TransactionType.YieldSupply.Enter || - type == TransactionType.YieldSupply.Exit || - type == TransactionType.YieldSupply.Topup - ) { - return "" + when (type) { + is TransactionType.Staking.Vote, + TransactionType.Staking.ClaimRewards, + TransactionType.Staking.Withdraw, + -> return "" + + is TransactionType.YieldSupply -> { + if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) { + return "" + } + } + else -> Unit } val prefix = when { status == TransactionStatus.Failed -> "" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt new file mode 100644 index 0000000000..dd2569e500 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class YieldSupplyPromoBannerKeyConverter( + private val yieldModuleApyMap: Map, + private val shouldShowMainPromo: Boolean, +) : Converter { + + override fun convert(value: TokenConverterParams): String? { + if (!shouldShowMainPromo) return null + + val currencies = when (value) { + is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies() + is TokenConverterParams.Account -> value.accountList.flattenCurrencies() + }.filter { status -> + status.value is CryptoCurrencyStatus.Loaded || + status.value is CryptoCurrencyStatus.Custom + } + + val tokens = currencies.filter { it.currency is CryptoCurrency.Token } + + if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + + val max = tokens.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val tokenKey = token.yieldSupplyKey() + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.second + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index 1a01d99ec3..d1cbb38064 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -5,16 +5,18 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.utils.coroutines.combine6 +import com.tangem.utils.coroutines.combine7 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import java.math.BigDecimal /** * Subscriber that monitors account list related data and updates the wallet state accordingly. @@ -30,19 +32,21 @@ internal class AccountListSubscriber @AssistedInject constructor( override val clickIntents: WalletClickIntents, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6( + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), flow6 = stakingApyFlow(), + flow7 = yieldSupplyGetShouldShowMainPromoFlow(), transform = ::updateState, ) - private fun yieldSupplyApyFlow(): Flow> { + private fun yieldSupplyApyFlow(): Flow> { return yieldSupplyApyFlowUseCase().distinctUntilChanged() } @@ -50,6 +54,10 @@ internal class AccountListSubscriber @AssistedInject constructor( return stakingApyFlowUseCase().distinctUntilChanged() } + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow { + return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged() + } + @AssistedFactory interface Factory { fun create(userWallet: UserWallet): AccountListSubscriber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 40a9681b13..080766b9b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -20,6 +20,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenCon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import timber.log.Timber +import java.math.BigDecimal /** * Basic implementation of [WalletSubscriber] for wallet with accounts. @@ -46,8 +47,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency: AppCurrency, expandedAccounts: Set, isAccountMode: Boolean, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean = false, ) { val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount @@ -67,6 +69,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { portfolioId = PortfolioId(mainAccount.accountId), yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } isAccountMode -> { @@ -82,7 +85,13 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } else { val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) + updateContent( + params = convertParams, + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, + ) } } } @@ -92,8 +101,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { maybeTokenList: Lce, appCurrency: AppCurrency, portfolioId: PortfolioId, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> @@ -123,14 +133,16 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { stateController.update( SetTokenListTransformer( @@ -140,6 +152,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index e827068c9b..36de78ef22 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker @@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal @Deprecated("Use AccountListSubscriber instead") @Suppress("LongParameterList") @@ -40,6 +42,7 @@ internal abstract class BasicTokenListSubscriber( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -69,7 +72,8 @@ internal abstract class BasicTokenListSubscriber( flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), flow4 = stakingApyFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap -> + flow5 = yieldSupplyGetShouldShowMainPromoFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -98,6 +102,7 @@ internal abstract class BasicTokenListSubscriber( appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) walletWithFundsChecker.check(tokenList) @@ -122,8 +127,9 @@ internal abstract class BasicTokenListSubscriber( private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map, + yieldSupplyApyMap: Map, stakingApyMap: Map>, + shouldShowMainPromo: Boolean, ) { stateHolder.update( SetTokenListTransformer( @@ -133,6 +139,7 @@ internal abstract class BasicTokenListSubscriber( clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } @@ -146,9 +153,12 @@ internal abstract class BasicTokenListSubscriber( } .distinctUntilChanged() - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() + private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() private fun stakingApyFlow(): Flow>> = stakingApyFlowUseCase() .distinctUntilChanged() + + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() + .distinctUntilChanged() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 36220cd6e1..d75d637dad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -12,6 +12,7 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -32,6 +33,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -41,6 +43,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e686dd4e20..9e148dcf24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -27,6 +28,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -36,6 +38,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index b9c65f6340..bef7b6defb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -5,6 +5,7 @@ import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet @@ -58,7 +59,7 @@ internal class TxHistorySubscriber( refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems) + setLoadedTxHistoryState(maybeTxHistoryItems, status.currency) } } } @@ -67,18 +68,19 @@ internal class TxHistorySubscriber( private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { stateHolder.update( maybeTxHistoryItemCount.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, - error = it, + error = error, pendingTransactions = status.value.pendingTransactions, + currency = status.currency, clickIntents = clickIntents, ) }, - ifRight = { + ifRight = { txCount -> SetTxHistoryCountTransformer( userWalletId = userWallet.walletId, - transactionsCount = it, + transactionsCount = txCount, clickIntents = clickIntents, ) }, @@ -86,7 +88,7 @@ internal class TxHistorySubscriber( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { stateHolder.update( maybeTxHistoryItems.fold( ifLeft = { @@ -102,6 +104,7 @@ internal class TxHistorySubscriber( symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, + currency = currency, ) SetTxHistoryItemsTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt index 23e7c78681..78c388f87f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt @@ -5,6 +5,7 @@ import androidx.paging.cachedIn import androidx.paging.map import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet @@ -51,7 +52,7 @@ internal class TxHistorySubscriberV2( refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems) + setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) } } } @@ -60,18 +61,19 @@ internal class TxHistorySubscriberV2( private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { stateController.update( maybeTxHistoryItemCount.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, - error = it, + error = error, pendingTransactions = status.value.pendingTransactions, clickIntents = clickIntents, + currency = status.currency, ) }, - ifRight = { + ifRight = { txCount -> SetTxHistoryCountTransformer( userWalletId = userWallet.walletId, - transactionsCount = it, + transactionsCount = txCount, clickIntents = clickIntents, ) }, @@ -79,7 +81,7 @@ internal class TxHistorySubscriberV2( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { stateController.update( maybeTxHistoryItems.fold( ifLeft = { @@ -92,6 +94,7 @@ internal class TxHistorySubscriberV2( ifRight = { itemsFlow -> val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() val itemConverter = TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt new file mode 100644 index 0000000000..458cbcaef2 --- /dev/null +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -0,0 +1,208 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.yield.supply.YieldSupplyStatus +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import org.junit.Test +import java.math.BigDecimal + +class YieldSupplyPromoBannerKeyConverterTest { + + @Test + fun `GIVEN promo disabled WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF") + val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) + val tokenList = ungroupedTokenList(status) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = tokenList, + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")), + shouldShowMainPromo = false, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN empty apy map WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") + val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = emptyMap(), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN active yield token present WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") + val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusActive), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.12")), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN multiple candidates EVM case insensitive WHEN convert THEN return key of max amount`() { + val evmNetworkId = "ETH" + val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd") + val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF") + + val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false) + val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) + + val apyMap = mapOf( + "${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), + "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"), + ) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusSmall, statusBig), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + val expectedKey = "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" + assertThat(result).isEqualTo(expectedKey) + } + + @Test + fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() { + val nonEvmId = "xrp" + val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123") + val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) + + val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}" + val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped { + return TokenList.Ungrouped( + totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded( + amount = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), + sortedBy = com.tangem.domain.models.TokensSortType.NONE, + currencies = statuses.toList(), + ) + } + + private fun createStatus( + token: CryptoCurrency.Token, + amount: BigDecimal, + isYieldActive: Boolean, + ): CryptoCurrencyStatus { + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "addr", + type = NetworkAddress.Address.Type.Primary, + ), + ) + val value = CryptoCurrencyStatus.Custom( + amount = amount, + fiatAmount = null, + fiatRate = null, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = if (isYieldActive) { + YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ) + } else { + null + }, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = networkAddress, + sources = CryptoCurrencyStatus.Sources(), + ) + return CryptoCurrencyStatus( + currency = token, + value = value, + ) + } + + private fun createToken(networkId: String, backendId: String, contract: String): CryptoCurrency.Token { + val network = Network( + id = Network.ID(value = networkId, derivationPath = Network.DerivationPath.None), + backendId = backendId, + name = backendId, + currencySymbol = "SYM", + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = when (backendId) { + "ethereum" -> Network.StandardType.ERC20 + else -> Network.StandardType.Unspecified("UNSPEC") + }, + hasFiatFeeRate = false, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(networkId), + suffix = CryptoCurrency.ID.Suffix.ContractAddress(contract), + ), + network = network, + name = "Token", + symbol = "TKN", + decimals = 18, + iconUrl = null, + isCustom = false, + contractAddress = contract, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt index 5acb41c803..038b9106de 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt @@ -20,4 +20,8 @@ internal interface WcCommonTransactionModel { fun showSuccessSignMessage(message: TextReference = resourceReference(R.string.wc_successfully_signed)) { messageSender.send(ToastMessage(message = message)) } + + fun showSuccessAddedMessage(message: TextReference = resourceReference(R.string.common_added)) { + messageSender.send(ToastMessage(message = message)) + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 08176516ce..8ed61a1b5d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -111,7 +111,7 @@ internal class WcAddNetworkModel @Inject constructor( modelScope.launch { _uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = true)) } useCase.approve().getOrNull()?.let { - showSuccessSignMessage() + showSuccessAddedMessage() router.pop() } ?: run { _uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = false)) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index 8dbc13d8c2..d468e6d60d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -58,6 +58,7 @@ internal class YieldSupplyActiveModel @Inject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val urlOpener: UrlOpener, private val appRouter: AppRouter, + private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, ) : Model(), YieldSupplyStopEarningComponent.ModelCallback, YieldSupplyApproveComponent.ModelCallback { @@ -241,11 +242,13 @@ internal class YieldSupplyActiveModel @Inject constructor( userWalletId, cryptoCurrencyStatusFlow.value, ).onRight { minAmount -> + val dustAmount = yieldSupplyGetDustMinAmountUseCase(minAmount = minAmount, appCurrency = appCurrency) uiState.update( YieldSupplyActiveMinAmountTransformer( cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value, appCurrency = appCurrency, minAmount = minAmount, + dustMinAmount = dustAmount, analyticsHandler = analyticsHandler, onApprove = ::onApprove, ), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt index c3b1fd4763..54dd971067 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt @@ -28,11 +28,12 @@ import java.math.BigDecimal * - Builds the fee policy note text using the minimum amount. * - Adds contextual notifications: * - Approval required notification when spending is not yet allowed (emits analytics on CTA). - * - "Not all amount supplied" info when wallet balance exceeds the supplied balance by more than [minAmount]. + * - "Not all amount supplied" info when the not-supplied balance exceeds the dust threshold [dustMinAmount]. * * @property cryptoCurrencyStatus Current currency status used to calculate values and flags. * @property appCurrency Preferred fiat currency for formatting. * @property minAmount Protocol-required minimal amount to deposit/supply (in crypto units). + * @property dustMinAmount Threshold used to detect dust/not-supplied balance (in crypto units). * @property analyticsHandler Analytics reporter for user actions. * @property onApprove Action invoked when the "Approve" notification button is tapped. */ @@ -40,6 +41,7 @@ internal class YieldSupplyActiveMinAmountTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, private val minAmount: BigDecimal, + private val dustMinAmount: BigDecimal, private val analyticsHandler: AnalyticsEventHandler, private val onApprove: () -> Unit, ) : Transformer { @@ -89,7 +91,7 @@ internal class YieldSupplyActiveMinAmountTransformer( } private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? { - return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)) { + return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustMinAmount)) { val cryptoCurrency = cryptoCurrencyStatus.currency val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull() val formattedAmount = diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 621430c377..c6bb211f29 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.yield.supply.impl.main.model +import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import android.os.SystemClock import com.tangem.common.routing.AppRoute.YieldSupplyPromo @@ -12,6 +13,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -51,6 +54,7 @@ internal class YieldSupplyModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventsHandler: AnalyticsEventHandler, private val appRouter: AppRouter, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, @@ -61,6 +65,7 @@ internal class YieldSupplyModel @Inject constructor( private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, + private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() @@ -69,6 +74,7 @@ internal class YieldSupplyModel @Inject constructor( field = MutableStateFlow(YieldSupplyUM.Initial) private val cryptoCurrency = params.cryptoCurrency + private var appCurrency: AppCurrency = AppCurrency.Default var userWallet: UserWallet by Delegates.notNull() private val fetchCurrencyJobHolder = JobHolder() @@ -81,10 +87,17 @@ internal class YieldSupplyModel @Inject constructor( } private fun checkIfYieldSupplyIsAvailable() { - modelScope.launch(dispatchers.io) { + modelScope.launch(dispatchers.default) { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency) if (isAvailable) { subscribeOnCurrencyStatusUpdates() + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = params.userWalletId, + network = cryptoCurrency.network, + ), + ) } } } @@ -334,7 +347,11 @@ internal class YieldSupplyModel @Inject constructor( val minAmount = yieldSupplyMinAmountUseCase(userWalletId = userWallet.walletId, cryptoCurrencyStatus) .getOrNull() if (minAmount != null) { - cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount) + val dustAmount = yieldSupplyGetDustMinAmountUseCase( + minAmount = minAmount, + appCurrency = appCurrency, + ) + cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount) } else { false } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8bb1e2ce03..d167ffb2eb 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31-1323" +tangemBlockchainSdk = "releases-5.31.1-1326" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.31-569" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^