diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 9338ab2914..408e20a9b6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -5,8 +5,10 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -106,4 +108,18 @@ object MarketsDomainModule { fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase { return GetTokenExchangesUseCase(marketsTokenRepository = marketsTokenRepository) } + + @Provides + @Singleton + fun provideGetStakingNotificationMaxApyUseCase( + settingsRepository: SettingsRepository, + promoRepository: PromoRepository, + marketsTokenRepository: MarketsTokenRepository, + ): GetStakingNotificationMaxApyUseCase { + return GetStakingNotificationMaxApyUseCase( + settingsRepository = settingsRepository, + promoRepository = promoRepository, + marketsTokenRepository = marketsTokenRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 7dc5634128..df25dd1255 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -226,5 +226,13 @@ internal object SettingsDomainModule { fun provideIsGooglePayAvailableUseCase(settingsRepository: SettingsRepository): IsGooglePayAvailableUseCase { return IsGooglePayAvailableUseCase(settingsRepository) } + + @Provides + @Singleton + fun provideMaybeSetWalletFirstTimeUsageUseCase( + settingsRepository: SettingsRepository, + ): SetWalletFirstTimeUsageUseCase { + return SetWalletFirstTimeUsageUseCase(settingsRepository) + } // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index c03a98f5ef..758979cd91 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -310,7 +310,7 @@ class VisaCardActivationTask @AssistedInject constructor( signature = response.signature, hash = dataToSign.hashToSign.hexToBytes(), publicKey = walletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() val signedActivationData = dataToSign.sign( rootOTP = otp.rootOTP.toHexString(), diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 232724142a..dc7b40ea9b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -176,7 +176,7 @@ class VisaCustomerWalletApproveTask( hash = hashToSign, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().lowercase() scanCard( session = session, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 0d3aaef26b..ab00fc7cfa 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -393,7 +393,7 @@ class WalletConnectSdkHelper { signature = signedHash, hash = hashToSign, publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().formatHex() + ).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE } } is CompletionResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt index 9f0bff8ab1..85e57a4ae2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt @@ -2,10 +2,8 @@ package com.tangem.tap.features.home import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -13,7 +11,7 @@ import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.SystemBarsIconsDisposable -import com.tangem.core.ui.res.LocalRootBackgroundColor +import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect import com.tangem.core.ui.utils.findActivity import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.api.HomeComponent @@ -65,17 +63,7 @@ internal class DefaultHomeComponent @AssistedInject constructor( onShopButtonClick = model::onShopClick, onSearchTokensClick = model::onSearchClick, ) - - val rootBackgroundColor = LocalRootBackgroundColor.current - val previousColor = remember { rootBackgroundColor.value } - val storiesBackgroundColor = Color(color = 0xFF010101) - - DisposableEffect(Unit) { - rootBackgroundColor.value = storiesBackgroundColor - onDispose { - rootBackgroundColor.value = previousColor - } - } + ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) } override fun newState(state: HomeState) { diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 91c45b832a..a3b184dc8e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -49,12 +49,12 @@ internal class HomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val getUserCountryUseCase: GetUserCountryUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val userWalletBuilderFactory: UserWalletBuilder.Factory, + getUserCountryUseCase: GetUserCountryUseCase, ) : Model() { private val tangemErrorHandler = TangemTangemErrorsHandler(store) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt index ad3c3c1c94..29d0277015 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt @@ -79,10 +79,7 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = resourceReference( - id = R.string.express_transaction_id, - formatArgs = wrappedList(link.text), - ).resolveReference(), + text = link.text.resolveReference(), maxLines = 1, overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.subtitle2, diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 79160bda24..51d3ecfaef 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -5,7 +5,7 @@ }, { "name": "ONRAMP_ENABLED", - "version": "undefined" + "version": "5.24.0" }, { "name": "VISA_ONBOARDING_ENABLED", @@ -37,7 +37,7 @@ }, { "name": "NEW_ARTWORK_LOADING", - "version": "5.24.0" + "version": "5.25.0" }, { "name": "NEW_ATTESTATION_ENABLED", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt index 9fc9554c4d..0caae8ba1a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt @@ -10,4 +10,13 @@ fun ApiResponse.fold(onSuccess: (T) -> R, onError: (ApiRes is ApiResponse.Error -> onError(cause) is ApiResponse.Success -> onSuccess(data) } +} + +inline fun catchApiResponseError(onError: (ApiResponseError) -> Unit, block: () -> T): T { + return try { + block() + } catch (e: ApiResponseError) { + onError(e) + throw e + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index b2af512cda..3c1829900b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -12,6 +12,7 @@ data class TokenMarketListResponse( @Json(name = "limit") val limit: Int, @Json(name = "offset") val offset: Int, @Json(name = "timestamp") val timestamp: Long? = null, + @Json(name = "summary") val summary: Summary? = null, ) { @JsonClass(generateAdapter = true) @@ -24,6 +25,7 @@ data class TokenMarketListResponse( @Json(name = "market_rating") val marketRating: Int?, @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, + @Json(name = "staking_opportunities") val stakingOpportunities: List?, ) { @JsonClass(generateAdapter = true) @@ -32,5 +34,27 @@ data class TokenMarketListResponse( @Json(name = "1w") val week1: BigDecimal?, @Json(name = "30d") val day30: BigDecimal?, ) + + @JsonClass(generateAdapter = true) + data class StakingOpportunities( + @Json(name = "id") val id: Int?, + @Json(name = "apy") val apy: BigDecimal?, + @Json(name = "network_id") val networkId: String?, + @Json(name = "reward_type") val rewardType: RewardType?, + ) + + @JsonClass(generateAdapter = false) + enum class RewardType { + @Json(name = "apy") APY, + + @Json(name = "apr") APR, + + UNKNOWN, + } } + + @JsonClass(generateAdapter = true) + data class Summary( + @Json(name = "max_apy") val maxApy: BigDecimal?, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 81b01211dc..0ac0fdc78a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -55,11 +55,11 @@ interface TangemTechApi { /** Returns referral status by [walletId] */ @GET("referral/{walletId}") - suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse + suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse /** Make user referral, requires [StartReferralBody] */ @POST("referral") - suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse + suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse @GET("quotes") suspend fun getQuotes( 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 90b07d055f..5e4f9577d1 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 @@ -86,6 +86,12 @@ object PreferencesKeys { val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } + val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { + booleanPreferencesKey(name = "marketsStakingNotificationHideClicked") + } + + val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") } + val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 52acdaa25f..eec41bdb9a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -321,7 +321,7 @@ private fun SecondaryPairButtons( } @Composable -internal fun CloseableIconButton( +fun CloseableIconButton( onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index c1df30c687..b6410efd58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -107,7 +107,7 @@ fun TangemTheme( CompositionLocalProvider( LocalTangemShimmer provides TangemShimmer, LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) }, - LocalRootBackgroundColor provides remember { mutableStateOf(rootBackgroundColor) }, + LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) }, LocalTextSelectionColors provides TangemTextSelectionColors, ) { ProvideTextStyle( diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/TangemThemeUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/TangemThemeUtils.kt new file mode 100644 index 0000000000..560034ee3b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/TangemThemeUtils.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.res.LocalRootBackgroundColor + +@Composable +fun ChangeRootBackgroundColorEffect(color: Color) { + var rootBackgroundColor by LocalRootBackgroundColor.current + var previousColor by remember { mutableStateOf(rootBackgroundColor) } + + DisposableEffect(color) { + rootBackgroundColor = color + onDispose { + rootBackgroundColor = previousColor + } + } + + LaunchedEffect(rootBackgroundColor) { + if (rootBackgroundColor != color) { + previousColor = rootBackgroundColor + rootBackgroundColor = color + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp b/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp new file mode 100644 index 0000000000..de2b304a0f Binary files /dev/null and b/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp differ diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index fa746e853f..dec987eadb 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { // endregion // region Others dependencies + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.jodatime) implementation(deps.moshi) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 5df2e53123..23806afa4e 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -27,7 +27,9 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import java.math.BigDecimal import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -41,6 +43,7 @@ internal class DefaultMarketsTokenRepository( private val excludedBlockchains: ExcludedBlockchains, private val cacheRegistry: CacheRegistry, private val tokenExchangesStore: RuntimeStateStore>, + private val maxApyStore: RuntimeStateStore, ) : MarketsTokenRepository { private val tokenMarketInfoConverter: TokenMarketInfoConverter = TokenMarketInfoConverter(excludedBlockchains) @@ -88,8 +91,12 @@ internal class DefaultMarketsTokenRepository( val last = res.tokens.size < request.limit + val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res) + + maxApyStore.store(tokenMarketListWithMaxApy.maxApy) + return BatchFetchResult.Success( - data = TokenMarketListConverter.convert(res), + data = tokenMarketListWithMaxApy.tokens, last = last, empty = res.tokens.isEmpty(), ) @@ -106,7 +113,7 @@ internal class DefaultMarketsTokenRepository( tangemTechApi = tangemTechApi, marketsApi = marketsApi, analyticsEventHandler = analyticsEventHandler, - onApiError = { + onApiResponseError = { analyticsEventHandler.send(createListErrorEvent(it).toEvent()) }, ) @@ -275,6 +282,10 @@ internal class DefaultMarketsTokenRepository( } } + override suspend fun getMaxApy(): Flow { + return maxApyStore.get() + } + inline fun catchListErrorAndSendEvent(block: () -> T): T { return catchErrorAndSendEvent(block, ::createListErrorEvent) } diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 6a9952d71f..3b5e662379 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -7,6 +7,7 @@ import com.tangem.data.markets.converters.TokenMarketChartsConverter import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.toRequestParam import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.catchApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse @@ -25,7 +26,7 @@ internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, private val analyticsEventHandler: AnalyticsEventHandler, - private val onApiError: (ApiResponseError) -> Unit, + private val onApiResponseError: (ApiResponseError) -> Unit, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( @@ -41,7 +42,7 @@ internal class MarketsBatchUpdateFetcher( val updateTasks = idsToUpdate.map { batchIds -> async { retryOnError { - catchApiError(onApiError) { + catchApiResponseError(onApiResponseError) { marketsApi.getCoinsListCharts( coinIds = batchIds.second.joinToString(separator = ","), interval = updateRequest.interval.toRequestParam(), @@ -71,7 +72,7 @@ internal class MarketsBatchUpdateFetcher( } is TokenMarketUpdateRequest.UpdateQuotes -> { val quotesRes = retryOnError { - catchApiError(onApiError) { + catchApiResponseError(onApiResponseError) { tangemTechApi.getQuotes( currencyId = updateRequest.currencyId, coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), @@ -133,13 +134,4 @@ internal class MarketsBatchUpdateFetcher( } } } - - private inline fun catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T { - return try { - block() - } catch (e: ApiResponseError) { - onError(e) - throw e - } - } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index 1875a5b609..e280e160e1 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -15,6 +15,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) TokenMarketListConfig.Order.Buyers -> "buyers" TokenMarketListConfig.Order.TopGainers -> "gainers" TokenMarketListConfig.Order.TopLosers -> "losers" + TokenMarketListConfig.Order.Staking -> "staking" } internal fun PriceChangeInterval.toRequestParam(): String = when (this) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 59d12b61c2..f8e13d53fc 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -2,22 +2,29 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListWithMaxApy import com.tangem.domain.markets.TokenQuotesShort import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isPositive -internal object TokenMarketListConverter : Converter> { +internal object TokenMarketListConverter : Converter { - override fun convert(value: TokenMarketListResponse): List { + override fun convert(value: TokenMarketListResponse): TokenMarketListWithMaxApy { val imageHost = value.imageHost ?: run { if (value.tokens.isEmpty()) { - return emptyList() + return TokenMarketListWithMaxApy(emptyList(), null) } else { error("imageHost cannot be null") } } - return value.tokens.map { token -> + val tokens = value.tokens.map { token -> + val stakingRate = token.stakingOpportunities + ?.mapNotNull { it.apy } + ?.max() + .takeIf { it?.isPositive() == true } + TokenMarket( id = CryptoCurrency.RawID(token.id), name = token.name, @@ -33,7 +40,9 @@ internal object TokenMarketListConverter : Converter { + return appPreferencesStore.get( + key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + default = false, + ) + } + + override suspend fun setMarketsStakingNotificationHideClicked() { + appPreferencesStore.store( + key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + value = true, + ) + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 87ea3f6639..8bc077f7c7 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -96,6 +96,17 @@ internal class DefaultSettingsRepository( } } + override suspend fun getWalletFirstUsageDate(): Long { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY, + default = 0L, + ) + } + + override suspend fun setWalletFirstUsageDate(value: Long) { + appPreferencesStore.store(key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY, value = value) + } + override suspend fun shouldShowMarketsTooltip(): Boolean { return appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index b848c5812f..cdff1f2b9e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -87,7 +87,7 @@ object LegacySdkHelper { signature = signedHash, hash = hashToSign, publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().formatHex() + ).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE fun createMessageData(message: String): ByteArray { val messageData = try { diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 7d820cb19a..735657a25b 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -23,9 +23,11 @@ dependencies { api(projects.domain.quotes) api(projects.domain.wallets) api(projects.domain.wallets.models) + api(projects.domain.promo) implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) + implementation(projects.domain.settings) api(projects.core.pagination) diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index e1a603f7f1..d2a80e71cf 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -12,6 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, + val stakingRate: BigDecimal?, private val imageHost: String, ) { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index c3633f3925..011df2c630 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -8,7 +8,7 @@ data class TokenMarketListConfig( ) { enum class Order { - ByRating, Trending, Buyers, TopGainers, TopLosers + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking } enum class Interval { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt new file mode 100644 index 0000000000..3b3bc77831 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarketListWithMaxApy( + val tokens: List, + val maxApy: BigDecimal?, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt new file mode 100644 index 0000000000..4ccaea6680 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.markets + +import android.icu.util.Calendar +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.settings.repositories.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import java.math.BigDecimal + +class GetStakingNotificationMaxApyUseCase( + private val settingsRepository: SettingsRepository, + private val promoRepository: PromoRepository, + private val marketsTokenRepository: MarketsTokenRepository, +) { + + suspend operator fun invoke(): Flow { + val hideClickedFlow = promoRepository.isMarketsStakingNotificationHideClicked() + val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate() + val currentDate = Calendar.getInstance().timeInMillis + + return combine( + flow = hideClickedFlow, + flow2 = marketsTokenRepository.getMaxApy(), + ) { hideClicked, maxApy -> + val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) { + currentDate - walletFirstUsageDate > TWO_WEEKS_IN_MILLIS + } else { + false + } + + maxApy.takeIf { showStakingNotification } + } + } + + private companion object { + const val TWO_WEEKS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index f7c26ceab6..8000c7fc53 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -3,6 +3,8 @@ package com.tangem.domain.markets.repositories import com.tangem.domain.markets.* import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal interface MarketsTokenRepository { @@ -51,4 +53,6 @@ interface MarketsTokenRepository { * @param tokenId token id */ suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List + + suspend fun getMaxApy(): Flow } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt index 0c59d82df8..2b5e6836cc 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampUpdateTransactionStatusUseCase.kt @@ -10,9 +10,15 @@ class OnrampUpdateTransactionStatusUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String, externalTxUrl: String, status: OnrampStatus.Status) = Either.catch { + suspend operator fun invoke( + txId: String, + externalTxId: String, + externalTxUrl: String, + status: OnrampStatus.Status, + ) = Either.catch { onrampTransactionRepository.updateTransactionStatus( txId = txId, + externalTxId = externalTxId, externalTxUrl = externalTxUrl, status = status, ) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt index 8eab66d2ef..b7d722cb81 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt @@ -14,7 +14,12 @@ interface OnrampTransactionRepository { fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> - suspend fun updateTransactionStatus(txId: String, externalTxUrl: String, status: OnrampStatus.Status) + suspend fun updateTransactionStatus( + txId: String, + externalTxId: String, + externalTxUrl: String, + status: OnrampStatus.Status, + ) suspend fun removeTransaction(txId: String) } \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 88682deae2..bed593a152 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -15,6 +15,10 @@ interface PromoRepository { suspend fun setNeverToShowWalletPromo(promoId: PromoId) suspend fun setNeverToShowTokenPromo(promoId: PromoId) + + suspend fun isMarketsStakingNotificationHideClicked(): Flow + + suspend fun setMarketsStakingNotificationHideClicked() // endregion // region Stories diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt new file mode 100644 index 0000000000..c0cef31d77 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.SettingsRepository +import java.util.Calendar + +class SetWalletFirstTimeUsageUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke() = Either.catch { + val savedTime = settingsRepository.getWalletFirstUsageDate() + + if (savedTime == 0L) { + settingsRepository.setWalletFirstUsageDate(Calendar.getInstance().timeInMillis) + } + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 23b4a1756c..1a64989894 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -30,6 +30,10 @@ interface SettingsRepository { suspend fun incrementAppLaunchCounter() + suspend fun getWalletFirstUsageDate(): Long + + suspend fun setWalletFirstUsageDate(value: Long) + suspend fun shouldShowMarketsTooltip(): Boolean suspend fun setMarketsTooltipShown(value: Boolean) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 40a8c174a9..2b1a377476 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.utils.extensions.isEqualTo @@ -15,13 +16,13 @@ class InvalidatePendingTransactionsUseCase( operator fun invoke( balanceItems: List, - processingActions: List, + stakingActions: List, token: Token, ): Either> { return Either.catch { val balancesToDisplay = mergeBalancesAndProcessingActions( realBalances = balanceItems, - processingActions = processingActions, + processingActions = stakingActions.filter { it.status == StakingActionStatus.PROCESSING }, token = token, ) balancesToDisplay @@ -68,6 +69,8 @@ class InvalidatePendingTransactionsUseCase( // intentionally do nothing } } + + doPostProcessing(balances, action, token) } return balances @@ -149,4 +152,15 @@ class InvalidatePendingTransactionsUseCase( } return index to action.amount } + + private fun doPostProcessing(balances: MutableList, action: StakingAction, token: Token) { + val validatorAddress = action.validatorAddress ?: action.validatorAddresses?.firstOrNull() + if (token.network == NetworkType.TON && validatorAddress != null) { + for (index in balances.indices) { + if (balances[index].validatorAddress == validatorAddress) { + balances[index] = balances[index].copy(isPending = true) + } + } + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt index 99f0faa3c3..f1916f48bb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -23,6 +23,7 @@ internal sealed class MarketsListAnalyticsEvent( SortByTypeUM.ExperiencedBuyers -> "Buyers" SortByTypeUM.TopGainers -> "Gainers" SortByTypeUM.TopLosers -> "Losers" + SortByTypeUM.Staking -> "Staking" }, "Period" to when (interval) { MarketsListUM.TrendInterval.H24 -> "24h" @@ -31,4 +32,19 @@ internal sealed class MarketsListAnalyticsEvent( }, ), ) + + data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + + data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") + + data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + + data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( + event = "Token Searched", + params = mapOf( + "Result" to if (tokenFound) "Yes" else "No", + ), + ) + + data object ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens") } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 66cddefe84..2018d1eb53 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.tokenlist.impl.model import androidx.compose.runtime.Stable +import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -8,8 +9,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase import com.tangem.domain.markets.TokenMarket import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.UserCountryError +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager @@ -21,8 +28,10 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import java.math.BigDecimal import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -31,23 +40,26 @@ private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @ModelScoped @Stable +@Suppress("LongParameterList") internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + private val promoRepository: PromoRepository, + private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private var updateQuotesJob = JobHolder() - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) private val visibleItemIds = MutableStateFlow>(emptyList()) @@ -57,7 +69,11 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, + onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, + onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, + onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) }, ) + private val mainMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -92,31 +108,60 @@ internal class MarketsListModel @Inject constructor( val state = marketsListUMStateManager.state.asStateFlow() init { - @Suppress("UnnecessaryParentheses") - modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .flatMapLatest { isInSearchMode -> - if (isInSearchMode) { - combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - ) { items, isError, notFound -> - (items to isError) to notFound - } - } else { - combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - ) { items, isError -> (items to isError) to false } + @Suppress("UnnecessaryParentheses") modelScope.launch { + marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> + if (isInSearchMode) { + combine( + searchMarketsListManager.uiItems, + searchMarketsListManager.isInInitialLoadingErrorState, + searchMarketsListManager.isSearchNotFoundState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = isSearchNotFoundState, + stakingNotificationMaxApy = stakingMaxApy, + userCountry = userCountry, + ) + } + } else { + combine( + mainMarketsListManager.uiItems, + mainMarketsListManager.isInInitialLoadingErrorState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = false, + stakingNotificationMaxApy = stakingNotificationMaxApy, + userCountry = userCountry, + ) } - }.collect { - marketsListUMStateManager.onUiItemsChanged( - uiItems = it.first.first, - isInErrorState = it.first.second, - isSearchNotFound = it.second, - ) } + }.collect { marketsItemsData -> + val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + } + + if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && + stakingNotificationMaxApy != null + ) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + } + + marketsListUMStateManager.onUiItemsChanged( + uiItems = marketsItemsData.items, + isInErrorState = marketsItemsData.isInErrorState, + isSearchNotFound = marketsItemsData.isSearchNotFound, + stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + }, + ) + } } state.onEach { @@ -126,29 +171,22 @@ internal class MarketsListModel @Inject constructor( }.launchIn(modelScope) // update all lists when user's currency has changed - currentAppCurrency - .drop(1) - .onEach { - mainMarketsListManager.reload() - if (marketsListUMStateManager.isInSearchState) { - searchMarketsListManager.reload() - } - }.launchIn(modelScope) + currentAppCurrency.drop(1).onEach { + mainMarketsListManager.reload() + if (marketsListUMStateManager.isInSearchState) { + searchMarketsListManager.reload() + } + }.launchIn(modelScope) // load charts when new batch is being loaded - mainMarketsListManager.onLastBatchLoadedSuccess - .onEach { - mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } - .launchIn(modelScope) + mainMarketsListManager.onLastBatchLoadedSuccess.onEach { + mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + }.launchIn(modelScope) // listen currently selected interval, update charts if sorting=rating, or reload all list modelScope.launch(dispatchers.default) { - marketsListUMStateManager.state - .map { it.selectedInterval } - .distinctUntilChanged() - .drop(1) + marketsListUMStateManager.state.map { it.selectedInterval }.distinctUntilChanged().drop(1) .collectLatest { interval -> when (marketsListUMStateManager.selectedSortByType) { SortByTypeUM.Rating -> { @@ -163,68 +201,64 @@ internal class MarketsListModel @Inject constructor( // reload list when sorting type has changed modelScope.launch { - marketsListUMStateManager.state - .map { it.selectedSortBy } - .distinctUntilChanged() - .drop(1) - .collectLatest { - mainMarketsListManager.reload() - } + marketsListUMStateManager.state.map { it.selectedSortBy }.distinctUntilChanged().drop(1).collectLatest { + mainMarketsListManager.reload() + } } // listen current visible batch and update charts modelScope.launch { - visibleItemIds - .mapNotNull { - if (it.isNotEmpty()) { - activeListManager.getBatchKeysByItemIds(visibleItemIds.value) - } else { - null - } - } - .distinctUntilChanged() - .collectLatest { visibleBatchKeys -> - // TODO load batch on scroll heat area - activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + visibleItemIds.mapNotNull { + if (it.isNotEmpty()) { + activeListManager.getBatchKeysByItemIds(visibleItemIds.value) + } else { + null } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + // TODO load batch on scroll heat area + activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + } } // ===Search=== modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .collectLatest { isInSearchMode -> - activeListManager = if (isInSearchMode) { - searchMarketsListManager - } else { - searchMarketsListManager.clearStateAndStopAllActions() - mainMarketsListManager - } + marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode -> + activeListManager = if (isInSearchMode) { + searchMarketsListManager + } else { + searchMarketsListManager.clearStateAndStopAllActions() + mainMarketsListManager } + } } modelScope.launch { - marketsListUMStateManager.searchQueryFlow - .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) - .distinctUntilChanged() - .onEach { + marketsListUMStateManager.searchQueryFlow.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) + .distinctUntilChanged().onEach { if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions() - } - .filter { it.isNotEmpty() && activeListManager == searchMarketsListManager } - .collectLatest { + }.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }.collectLatest { searchMarketsListManager.reload(searchText = it) } } modelScope.launch { - searchMarketsListManager - .onLastBatchLoadedSuccess - .collectLatest { - searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } + searchMarketsListManager.onLastBatchLoadedSuccess.collectLatest { + searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + } } + searchMarketsListManager.isSearchNotFoundState.onEach { + if (it) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) + } + }.launchIn(modelScope) + + searchMarketsListManager.onFirstBatchLoadedSuccess.onEach { + analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = true)) + }.launchIn(modelScope) + // analytics initAnalytics() @@ -233,17 +267,14 @@ internal class MarketsListModel @Inject constructor( } private fun initAnalytics() { - containerBottomSheetState - .onEach { - if (it == BottomSheetState.EXPANDED) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) - } - }.launchIn(modelScope) + containerBottomSheetState.onEach { + if (it == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + } + }.launchIn(modelScope) - state - .filter { it.isInSearchMode.not() } - .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) } - .distinctUntilChanged() + state.filter { it.isInSearchMode.not() } + .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged() .onEach { analyticsEventHandler.send(it) }.launchIn(modelScope) @@ -270,4 +301,19 @@ internal class MarketsListModel @Inject constructor( } }.saveIn(updateQuotesJob) } + + private fun onStakingNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + modelScope.launch { + promoRepository.setMarketsStakingNotificationHideClicked() + } + } + + private class MarketsItemsData( + val items: ImmutableList, + val isInErrorState: Boolean, + val isSearchNotFound: Boolean, + val stakingNotificationMaxApy: BigDecimal?, + val userCountry: Either, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c4adb37979..d7352826b5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -5,6 +5,8 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.compact import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -12,6 +14,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.utils.converter.Converter @@ -37,8 +40,11 @@ internal class MarketsTokenItemConverter( price = value.getCurrentPrice(), trendPercentText = value.getTrendPercent(), trendType = value.getTrendType(), - chardData = value.getChartData(), + chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.stakingRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, ) } @@ -64,7 +70,7 @@ internal class MarketsTokenItemConverter( prevUI.trendPercentText, ) { new.getTrendPercent() }, trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, - chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index 37e30253a3..03b8b24eeb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -84,6 +84,25 @@ internal class MarketsListBatchFlowManager( } } + val onFirstBatchLoadedSuccess = batchFlow.state + .distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size } + .mapNotNull { + when (val status = it.status) { + is PaginationStatus.Paginating -> { + if (status.lastResult is BatchFetchResult.Success) { + it.data.size == 1 + } else { + null + } + } + is PaginationStatus.EndOfPagination -> { + it.data.size == 1 + } + else -> null + } + } + .filter { it } + val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() @@ -323,6 +342,7 @@ internal class MarketsListBatchFlowManager( SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers + SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 45e5ac7d4f..f2c6ee64d3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -16,14 +16,19 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import java.math.BigDecimal @Stable +@Suppress("LongParameterList") internal class MarketsListUMStateManager( private val currentVisibleIds: Provider>, private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, + private val onStakingNotificationClick: () -> Unit, + private val onStakingNotificationCloseClick: () -> Unit, + private val onShowTokensUnder100kClicked: () -> Unit, ) { private var sortByBottomSheetIsShown @@ -87,6 +92,7 @@ internal class MarketsListUMStateManager( isInErrorState: Boolean, isSearchNotFound: Boolean, uiItems: ImmutableList, + stakingNotificationMaxApy: BigDecimal?, ) { state.update { when { @@ -102,13 +108,19 @@ internal class MarketsListUMStateManager( it.copy(list = ListUM.Loading) } else -> { - it.updateItems(newItems = uiItems) + it.updateItems( + newItems = uiItems, + stakingNotificationMaxApy = stakingNotificationMaxApy, + ) } } } } - private fun MarketsListUM.updateItems(newItems: ImmutableList): MarketsListUM { + private fun MarketsListUM.updateItems( + newItems: ImmutableList, + stakingNotificationMaxApy: BigDecimal?, + ): MarketsListUM { val currentState = this if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) { @@ -119,6 +131,7 @@ internal class MarketsListUMStateManager( .copy( showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), ), + stakingNotificationMaxApy = stakingNotificationMaxApy, ) } @@ -136,6 +149,7 @@ internal class MarketsListUMStateManager( showUnder100kTokensNotificationWasHidden = false, showUnder100kTokensNotification = true, onShowTokensUnder100kClicked = { + onShowTokensUnder100kClicked() state.update { s -> (s.list as? ListUM.Content)?.let { s.copy( @@ -211,6 +225,12 @@ internal class MarketsListUMStateManager( onOptionClicked = ::onBottomSheetOptionClicked, ), ), + stakingNotificationMaxApy = null, + onStakingNotificationClick = { + onStakingNotificationClick() + selectedSortByType = SortByTypeUM.Staking + }, + onStakingNotificationCloseClick = onStakingNotificationCloseClick, ) private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index b8726678a0..a0f8508f89 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.buildAnnotatedString import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 @@ -29,10 +30,11 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -41,6 +43,7 @@ import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet +import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM @@ -48,6 +51,9 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +private const val SHOW_MORE_KEY = "privacyPolicy" @Composable internal fun MarketsList( @@ -110,14 +116,50 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi SpacerH12() } } - AnimatedVisibility(state.isInSearchMode.not()) { - Options( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) + Column { + AnimatedVisibility(state.isInSearchMode.not()) { + Options( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + + AnimatedVisibility( + state.isInSearchMode.not() && + state.stakingNotificationMaxApy != null && + state.selectedSortBy != SortByTypeUM.Staking, + ) { + val showMore = stringResourceSafe(R.string.common_show_more) + val description = stringResourceSafe( + R.string.markets_staking_banner_description_placeholder, + showMore, + ) + + val clickableDescription = buildAnnotatedString { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + StakingInMarketsPromoNotification( + config = NotificationConfig( + iconResId = R.drawable.img_staking_in_market_notification, + title = resourceReference( + R.string.markets_staking_banner_title, + wrappedList(state.stakingNotificationMaxApy.format { percent() }), + ), + subtitle = annotatedReference(clickableDescription), + onClick = state.onStakingNotificationClick, + onCloseClick = state.onStakingNotificationCloseClick, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) + } } } val strokeWidth = TangemTheme.dimens.size0_5 @@ -326,6 +368,9 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), + stakingNotificationMaxApy = BigDecimal(0.12345), + onStakingNotificationClick = {}, + onStakingNotificationCloseClick = {}, ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index c153024da0..4c8ed29713 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -22,6 +23,8 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -95,6 +98,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier .alignByBaseline(), ratingPosition = model.ratingPosition, marketCap = model.marketCap, + stakingRate = model.stakingRate, ) PriceChangeInPercent( modifier = Modifier.alignByBaseline(), @@ -110,7 +114,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier Chart( chartType = model.chartType, - chartRawData = model.chardData, + chartRawData = model.chartData, ) } } @@ -142,7 +146,12 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier } @Composable -private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) { +private fun TokenSubtitle( + ratingPosition: String?, + marketCap: String?, + stakingRate: TextReference?, + modifier: Modifier = Modifier, +) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, @@ -150,6 +159,10 @@ private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: TokenRatingPlace(ratingPosition = ratingPosition) SpacerW4() TokenMarketCapText(text = marketCap ?: "") + if (stakingRate != null) { + SpacerW4() + StakingRate(stakingRate = stakingRate.resolveReference()) + } } } @@ -174,6 +187,28 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) { } } +@Composable +private fun RowScope.StakingRate(stakingRate: String) { + Box( + modifier = Modifier + .alignByBaseline() + .heightIn(min = TangemTheme.dimens.size16) + .border( + width = TangemTheme.dimens.size1, + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersSmall2, + ) + .padding(horizontal = TangemTheme.dimens.spacing5), + ) { + Text( + text = stakingRate, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + @Composable private fun RowScope.TokenMarketCapText(text: String) { Text( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt new file mode 100644 index 0000000000..9026a0ebb4 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt @@ -0,0 +1,147 @@ +package com.tangem.features.markets.tokenlist.impl.ui.components + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.notifications.CloseableIconButton +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +private val bgColor = Color(0x1F8CD9FF) +private val borderColor = Color(0x3D8CD9FF) + +@Composable +fun StakingInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + var textHeightDp by remember { mutableStateOf(0.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = borderColor, + ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(bgColor) + .clickable { config.onClick?.invoke() }, + ) { + PromoImage( + iconRes = config.iconResId, + modifier = Modifier.height(textHeightDp), + ) + PromoText( + title = config.title, + subtitle = config.subtitle, + onSizeChange = { textHeightDp = it }, + ) + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.secondary, + ) + } +} + +@Composable +private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .requiredWidth(56.dp) + .wrapContentHeight(Alignment.Top, unbounded = true), + ) + } +} + +@Composable +private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) { + val density = LocalDensity.current + + Box( + modifier = Modifier.onSizeChanged { + with(density) { onSizeChange(it.height.toDp()) } + }, + ) { + TextsBlock( + title = title, + subtitle = subtitle, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.CenterStart) + .padding(start = 76.dp, top = 12.dp, end = 12.dp, bottom = 12.dp), + ) + } +} + +@Composable +private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val titleText = title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + SpacerH(height = TangemTheme.dimens.spacing2) + } + + Text( + text = subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.caption2, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun RingPromoNotification_Preview() { + TangemThemePreview { + StakingInMarketsPromoNotification( + config = NotificationConfig( + title = stringReference("Earn up to 14% APY"), + subtitle = stringReference("Staking is the easiest way to earn rewards on your crypto. Show more"), + iconResId = R.drawable.img_staking_in_market_notification, + onCloseClick = { }, + ), + ) + } +} + +// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 9c2b0bb663..3371a2f40d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -1,9 +1,11 @@ @file:Suppress("MagicNumber") + package com.tangem.features.markets.tokenlist.impl.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf @@ -20,10 +22,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -35,8 +38,9 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.NEUTRAL, - chardData = null, + chartData = null, isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -48,10 +52,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.DOWN, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -63,10 +68,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -78,10 +84,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), MarketsListItemUM( id = CryptoCurrency.RawID("1"), @@ -93,10 +100,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, - chardData = MarketChartRawData( + chartData = MarketChartRawData( y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), isUnder100kMarketCap = false, + stakingRate = stringReference("APY 12.34%"), ), ), ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 62e200f011..5f29995bff 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency @Immutable @@ -17,8 +18,9 @@ data class MarketsListItemUM( val price: Price, val trendPercentText: String, val trendType: PriceChangeType, - val chardData: MarketChartRawData?, + val chartData: MarketChartRawData?, val isUnder100kMarketCap: Boolean, + val stakingRate: TextReference?, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP -> MarketChartLook.Type.Growing diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index 47de369206..da29b428d1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal internal data class MarketsListUM( val list: ListUM, @@ -18,6 +19,9 @@ internal data class MarketsListUM( val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, + val stakingNotificationMaxApy: BigDecimal?, + val onStakingNotificationClick: () -> Unit, + val onStakingNotificationCloseClick: () -> Unit, ) { val isInSearchMode get() = searchBar.isActive @@ -35,6 +39,7 @@ enum class SortByTypeUM(val text: TextReference) { ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)), TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), + Staking(resourceReference(R.string.common_staking)), } @Immutable diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt index 05e2c4fd39..bd7b6d1a54 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/WalletCard.kt @@ -15,7 +15,7 @@ internal fun WalletCard(artwork: ArtworkUM?, modifier: Modifier = Modifier) { AsyncImage( modifier = modifier, model = ImageRequest.Builder(LocalContext.current) - .data(artwork?.verifiedArtwork ?: artwork?.defaultUrl) + .data(artwork?.verifiedArtwork?.toByteArray() ?: artwork?.defaultUrl) .crossfade(true) .build(), placeholder = painterResource(R.drawable.card_placeholder_black), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt index 65dd778fc4..c8a1c938fd 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt @@ -21,8 +21,8 @@ internal inline fun OnboardingEntry( Column( modifier = modifier .fillMaxSize() - .statusBarsPadding() - .background(TangemTheme.colors.background.primary), + .background(TangemTheme.colors.background.primary) + .statusBarsPadding(), ) { stepperContent(Modifier.fillMaxWidth()) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 12371bbef8..8ff42661ed 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -94,6 +94,10 @@ internal class OnrampMainComponentModel @Inject constructor( userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) + modelScope.launch { + clearOnrampCacheUseCase() + } + sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt index cb5a0cd776..be7c79fafc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.success.entity import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.onramp.model.OnrampStatus sealed class OnrampSuccessComponentUM { @@ -18,5 +19,6 @@ sealed class OnrampSuccessComponentUM { val providerName: TextReference, val providerImageUrl: String, val notification: NotificationUM?, + val activeStatus: OnrampStatus.Status, ) : OnrampSuccessComponentUM() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index 07d34bce79..d7834c622a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -50,8 +50,8 @@ internal class SetOnrampSuccessContentConverter( statusBlock = convertStatuses( status = value.status, externalTxId = value.externalTxId, - externalTxUrl = value.externalTxUrl, ), + activeStatus = value.status, notification = getNotification(status = value.status, externalTxUrl = value.externalTxUrl), ) } @@ -78,11 +78,7 @@ internal class SetOnrampSuccessContentConverter( null } - private fun convertStatuses( - status: OnrampStatus.Status, - externalTxId: String?, - externalTxUrl: String?, - ): ExpressStatusUM { + private fun convertStatuses(status: OnrampStatus.Status, externalTxId: String?): ExpressStatusUM { val statuses = with(status) { persistentListOf( getAwaitingDepositItem(), @@ -94,7 +90,7 @@ internal class SetOnrampSuccessContentConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status = status, externalTxId = externalTxId, externalTxUrl = externalTxUrl), + link = getStatusLink(externalTxId = externalTxId), statuses = statuses, ) } @@ -165,7 +161,11 @@ internal class SetOnrampSuccessContentConverter( resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name)) } }, - state = getStatusState(OnrampStatus.Status.Paid), + state = when { + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error + else -> getStatusState(OnrampStatus.Status.Paid) + }, ) private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( @@ -189,36 +189,22 @@ internal class SetOnrampSuccessContentConverter( resourceReference(R.string.express_exchange_status_sent, wrappedList(cryptoCurrency.name)) } }, - state = getStatusState(OnrampStatus.Status.Sending), + state = when { + this == OnrampStatus.Status.RefundInProgress -> ExpressStatusItemState.Active + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Done + else -> getStatusState(OnrampStatus.Status.Sending) + }, ) - private fun getStatusLink( - status: OnrampStatus.Status, - externalTxId: String?, - externalTxUrl: String?, - ): ExpressLinkUM { - if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - goToProviderClick(externalTxUrl) - }, - ) - } - else -> if (externalTxId != null) { - ExpressLinkUM.Content( - icon = R.drawable.ic_copy_24, - text = stringReference(externalTxId), - onClick = { onCopyClick(externalTxId) }, - ) - } else { - ExpressLinkUM.Empty - } + private fun getStatusLink(externalTxId: String?): ExpressLinkUM { + return if (externalTxId != null) { + ExpressLinkUM.Content( + icon = R.drawable.ic_copy_24, + text = resourceReference(R.string.express_transaction_id, wrappedList(externalTxId)), + onClick = { onCopyClick(externalTxId) }, + ) + } else { + ExpressLinkUM.Empty } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt index d7e355ba63..a984b3db97 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt @@ -6,6 +6,7 @@ import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM import kotlinx.collections.immutable.persistentListOf @@ -24,6 +25,7 @@ internal data object OnrampSuccessComponentUMPreviewData { toAmount = stringReference("99.99 $"), currencyImageUrl = "", notification = null, + activeStatus = OnrampStatus.Status.Verifying, statusBlock = ExpressStatusUM( title = resourceReference(R.string.express_exchange_status_title), link = ExpressLinkUM.Empty, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 98e4fe4a28..eed7f917c0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.impl.R @@ -29,6 +30,8 @@ import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.PeriodicTask +import com.tangem.utils.coroutines.SingleTaskScheduler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates @Suppress("LongParameterList") internal class OnrampSuccessComponentModel @Inject constructor( @@ -60,6 +64,10 @@ internal class OnrampSuccessComponentModel @Inject constructor( val state: StateFlow get() = _state.asStateFlow() + private var userWallet: UserWallet by Delegates.notNull() + private var cryptoCurrency: CryptoCurrency by Delegates.notNull() + private var expressTxStatusTaskScheduler = SingleTaskScheduler() + init { loadData() } @@ -82,67 +90,87 @@ internal class OnrampSuccessComponentModel @Inject constructor( showErrorAlert(error) }, ifRight = { transaction -> - loadTransactionStatus(transaction) + userWallet = getUserWalletUseCase(transaction.userWalletId).getOrElse { + Timber.e("UserWallet found") + // this case should never happened + showErrorAlert(OnrampError.DomainError("UserWallet not found")) + return@launch + } + cryptoCurrency = getCryptoCurrencyUseCase( + userWallet = userWallet, + cryptoCurrencyId = transaction.toCurrencyId, + ).getOrElse { + Timber.e("Crypto currency not found") + showErrorAlert(OnrampError.DomainError(null)) + return@launch + } + + startStatusUpdateTask(transaction) }, ) } } - private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { - val userWallet = getUserWalletUseCase(transaction.userWalletId).getOrNull() - if (userWallet == null) { - Timber.e("UserWallet found") - // this case should never happened - showErrorAlert(OnrampError.DomainError("UserWallet not found")) - return - } - val cryptoCurrency = getCryptoCurrencyUseCase( - userWallet = userWallet, - cryptoCurrencyId = transaction.toCurrencyId, - ).getOrElse { - Timber.e("Crypto currency not found") - showErrorAlert(OnrampError.DomainError(null)) - return - } - - getOnrampStatusUseCase(userWallet = userWallet, txId = transaction.txId) - .fold( - ifLeft = { error -> - analyticsEventHandler.sendOnrampErrorEvent( - error = error, - tokenSymbol = cryptoCurrency.symbol, - providerName = transaction.providerName, - paymentMethod = transaction.paymentMethod, - ) - Timber.e(error.toString()) - showErrorAlert(error) - }, - ifRight = { status -> - analyticsEventHandler.send( - OnrampAnalyticsEvent.SuccessScreenOpened( - providerName = transaction.providerName, - currency = transaction.fromCurrency.code, - tokenSymbol = cryptoCurrency.symbol, - residence = transaction.residency, - paymentMethod = transaction.paymentMethod, - ), - ) - _state.update { - SetOnrampSuccessContentConverter( - cryptoCurrency = cryptoCurrency, - transaction = transaction, - goToProviderClick = ::goToProviderClick, - onCopyClick = ::onCopyClick, - ).convert(status) + private fun startStatusUpdateTask(transaction: OnrampTransaction) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + modelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + loadTransactionStatus(transaction) } - removeTransactionIfTerminalStatus( - cryptoCurrency = cryptoCurrency, - providerName = transaction.providerName, - paymentMethod = transaction.paymentMethod, - status = status, - ) }, - ) + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) + } + + private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { + val isTerminal = (state.value as? OnrampSuccessComponentUM.Content)?.activeStatus?.isTerminal + if (isTerminal == null || isTerminal == false) { + getOnrampStatusUseCase(userWallet = userWallet, txId = transaction.txId) + .fold( + ifLeft = { error -> + analyticsEventHandler.sendOnrampErrorEvent( + error = error, + tokenSymbol = cryptoCurrency.symbol, + providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, + ) + Timber.e(error.toString()) + showErrorAlert(error) + }, + ifRight = { status -> + analyticsEventHandler.send( + OnrampAnalyticsEvent.SuccessScreenOpened( + providerName = transaction.providerName, + currency = transaction.fromCurrency.code, + tokenSymbol = cryptoCurrency.symbol, + residence = transaction.residency, + paymentMethod = transaction.paymentMethod, + ), + ) + _state.update { + SetOnrampSuccessContentConverter( + cryptoCurrency = cryptoCurrency, + transaction = transaction, + goToProviderClick = ::goToProviderClick, + onCopyClick = ::onCopyClick, + ).convert(status) + } + removeTransactionIfTerminalStatus( + cryptoCurrency = cryptoCurrency, + providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, + status = status, + ) + }, + ) + } } private fun showErrorAlert(error: OnrampError) { @@ -180,4 +208,8 @@ internal class OnrampSuccessComponentModel @Inject constructor( } } } + + private companion object { + const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L + } } \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index e5e17ce4e9..c1c05b9e40 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -38,7 +39,7 @@ internal class ReferralRepositoryImpl @Inject constructor( val referralData = referralConverter.convert( referralApi.getReferralStatus( walletId = walletId, - ), + ).getOrThrow(), ) referralStatus[walletId] = referralData @@ -66,7 +67,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenId = tokenId, address = address, ), - ), + ).getOrThrow(), ) referralStatus[walletId] = referralData referralData diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index a5d37a1b19..20a2fe5ecd 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -4,11 +4,12 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { - object ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") - object ClickParticipate : ReferralEvents(event = "Button - Participate") - object ClickCopy : ReferralEvents(event = "Button - Copy") - object ClickShare : ReferralEvents(event = "Button - Share") - object ClickTaC : ReferralEvents(event = "Link - TaC") + data object ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") + data object ClickParticipate : ReferralEvents(event = "Button - Participate") + data object ClickCopy : ReferralEvents(event = "Button - Copy") + data object ClickShare : ReferralEvents(event = "Button - Share") + data object ClickTaC : ReferralEvents(event = "Link - TaC") + data object ParticipateSuccessful : ReferralEvents(event = "Participate Successful") } private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index a2379d3f8a..943c75b40d 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -91,7 +91,10 @@ internal class ReferralModel @Inject constructor( uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { runCatching { referralInteractor.startReferral(params.userWalletId) } - .onSuccess(::showContent) + .onSuccess { + analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) + showContent(it) + } .onFailure { throwable -> if (throwable is ReferralError.UserCancelledException) { lastReferralData?.let { referralData -> diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 2029309da1..77bf7443e4 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -39,7 +39,7 @@ internal data class ReferralStateHolder( val onParticipateClicked: () -> Unit, ) : ReferralInfoState, ReferralInfoContentState - object Loading : ReferralInfoState + data object Loading : ReferralInfoState } data class ErrorSnackbar( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt index ef94394fcf..9ebfa54ccf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt @@ -9,7 +9,7 @@ sealed class PredefinedValues { abstract val memo: String? data class Deeplink( - override val amount: String?, + override val amount: String, override val address: String, override val memo: String?, val transactionId: String, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt index 70d852dff8..4c50af08d0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt @@ -113,7 +113,7 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo iconResId = wrappedPairButtonsUM.leftIconResId!!, onClick = { singleEvent { - wrappedPairButtonsUM.onLeftClick + wrappedPairButtonsUM.onLeftClick() } }, modifier = Modifier.weight(1f), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 3f2eba602f..36b57ddd7c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -21,7 +21,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.SendContent @@ -252,20 +251,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent { val cryptoCurrencyStatus = model.cryptoCurrencyStatus val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus - val predefinedAmount = params.amount - val predefinedTxId = params.transactionId - val predefinedAddress = params.destinationAddress - val predefinedValues = - if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { - PredefinedValues.Content.Deeplink( - amount = predefinedAmount, - address = predefinedAddress, - memo = params.tag, - transactionId = predefinedTxId, - ) - } else { - PredefinedValues.Empty - } + return if (cryptoCurrencyStatus != null && feeCryptoCurrencyStatus != null) { SendConfirmComponent( appComponentContext = factoryContext, @@ -279,7 +265,7 @@ internal class DefaultSendComponent @AssistedInject constructor( feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, appCurrency = model.appCurrency, callback = model, - predefinedValues = predefinedValues, + predefinedValues = model.predefinedValues, onLoadFee = model::loadFee, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index ef29dbab61..edfc1b27ca 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -109,6 +110,7 @@ internal class SendModel @Inject constructor( subscribeOnQRScannerResult() subscribeOnCurrencyStatusUpdates() initAppCurrency() + initPredefinedValues() } override fun onNavigationResult(navigationUM: NavigationUM) { @@ -133,19 +135,31 @@ internal class SendModel @Inject constructor( } suspend fun loadFee(): Either { - val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") - val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") - val enteredDestinationAddress = destinationUM.addressTextField.value - val enteredMemo = destinationUM.memoTextField?.value - val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") + val predefinedValues = predefinedValues + val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) { + val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()?.convertToSdkAmount(cryptoCurrency) + createTransferTransactionUseCase( + amount = predefinedAmount ?: error("Invalid amount"), + memo = predefinedValues.memo, + destination = predefinedValues.address, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + } else { + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") + val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") + val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredMemo = destinationUM.memoTextField?.value + val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") - val transferTransaction = createTransferTransactionUseCase( - amount = enteredAmount.convertToSdkAmount(cryptoCurrency), - memo = enteredMemo, - destination = enteredDestinationAddress, - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - ).getOrElse { + createTransferTransactionUseCase( + amount = enteredAmount.convertToSdkAmount(cryptoCurrency), + memo = enteredMemo, + destination = enteredDestinationAddress, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + }.getOrElse { return GetFeeError.DataError(it).left() } @@ -156,16 +170,6 @@ internal class SendModel @Inject constructor( ) } - private fun resetPredefinedAmount() { - // reset predefined amount - val internalPredefinedValues = predefinedValues - predefinedValues = when (internalPredefinedValues) { - is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = null) - is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null) - PredefinedValues.Empty -> internalPredefinedValues - } - } - fun showAlertError() { sendConfirmAlertFactory.getGenericErrorState( onFailedTxEmailClick = ::onFailedTxEmailClick, @@ -179,6 +183,34 @@ internal class SendModel @Inject constructor( } } + private fun initPredefinedValues() { + val predefinedAmount = params.amount + val predefinedTxId = params.transactionId + val predefinedAddress = params.destinationAddress + + predefinedValues = if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { + PredefinedValues.Content.Deeplink( + amount = predefinedAmount, + address = predefinedAddress, + memo = params.tag, + transactionId = predefinedTxId, + ) + } else { + PredefinedValues.Empty + } + } + + private fun resetPredefinedAmount() { + // reset predefined amount + val internalPredefinedValues = predefinedValues + predefinedValues = when (internalPredefinedValues) { + is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null) + is PredefinedValues.Content.Deeplink, + PredefinedValues.Empty, + -> internalPredefinedValues + } + } + private fun subscribeOnCurrencyStatusUpdates() { modelScope.launch { getUserWalletUseCase(params.userWalletId).fold( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 5bc3264f89..570cb197f7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -159,8 +159,6 @@ internal class SendFeeModel @Inject constructor( saveResult() } - } else { - saveResult() } } @@ -198,6 +196,7 @@ internal class SendFeeModel @Inject constructor( ) val params = params as? SendFeeComponentParams.FeeParams ?: return params.callback.onFeeResult(uiState.value) + params.onNextClick() } private fun checkLoadFee() { @@ -272,10 +271,7 @@ internal class SendFeeModel @Inject constructor( primaryButton = ButtonsUM.PrimaryButtonUM( text = resourceReference(R.string.common_continue), isEnabled = state.isPrimaryButtonEnabled, - onClick = { - onNextClick() - params.onNextClick() - }, + onClick = ::onNextClick, ), prevButton = null, secondaryPairButtonsUM = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 62bab48774..6c4ea5950f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -145,7 +145,7 @@ internal class StakingModel @Inject constructor( } private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var processingActions: List = emptyList() + private var stakingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var minimumTransactionAmount: EnterAmountBoundary? = null @@ -157,7 +157,7 @@ internal class StakingModel @Inject constructor( val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data return invalidatePendingTransactionsUseCase( balanceItems = yieldBalance?.balance?.items ?: emptyList(), - processingActions = processingActions, + stakingActions = stakingActions, token = yield.token, ).getOrElse { emptyList() } } @@ -1028,7 +1028,7 @@ internal class StakingModel @Inject constructor( .distinctUntilChanged() .onEach { result -> result.getOrNull()?.let { actions -> - processingActions = actions + stakingActions = actions if (isInitState()) { updateInitialData(status) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 9a8549382c..9d5d52d214 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -11,8 +11,7 @@ import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.YieldReward -import com.tangem.lib.crypto.BlockchainUtils.isBSC -import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -87,7 +86,7 @@ internal class YieldBalancesConverter( val isRewardsClaimable = rewards?.isNotEmpty() == true return when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isStakingRewardUnavailable(blockchainId) -> RewardBlockType.RewardUnavailable isRewardsClaimable && isActionable -> RewardBlockType.Rewards isRewardsClaimable && !isActionable -> RewardBlockType.RewardsRequirementsError else -> RewardBlockType.NoRewards diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 00e36b4d44..122420439f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -11,7 +11,6 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionN import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency @@ -23,7 +22,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -117,6 +115,7 @@ internal class AddStakingNotificationsTransformer( sendingAmount = sendingAmount, actionAmount = amountValue, feeValue = feeValue, + tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD, ) }.toImmutableList() @@ -178,9 +177,7 @@ internal class AddStakingNotificationsTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, onClick = prevState.clickIntents::openTokenDetails, ) - addTonUnstakeNotification( - actionType = prevState.actionType, - ) + addTonExtraFeeErrorNotification() addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -276,20 +273,12 @@ internal class AddStakingNotificationsTransformer( } } - private fun MutableList.addTonUnstakeNotification(actionType: StakingActionCommonType) { + private fun MutableList.addTonExtraFeeErrorNotification() { val amount = cryptoCurrencyStatusProvider().value.amount.orZero() val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId - if (isTon(cryptoCurrencyNetworkIdValue) && actionType !is StakingActionCommonType.Enter) { - val notification = if (amount < TON_BALANCE_EXTRA_FEE_THRESHOLD) { - NotificationUM.Error.TonStakingExtraFeeError - } else { - StakingNotification.Info.Ordinary( - title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), - text = resourceReference(R.string.staking_notification_ton_extra_reserve_info), - ) - } - add(notification) + if (isTon(cryptoCurrencyNetworkIdValue) && amount < TON_BALANCE_EXTRA_FEE_THRESHOLD) { + add(NotificationUM.Error.TonStakingExtraFeeError) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 1690ae4a9f..10e064d094 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -37,14 +37,17 @@ internal class StakingInfoNotificationsFactory( * @param actionAmount any amount being transferred or used action * @param feeValue fee amount payed from user account */ + @Suppress("LongParameterList") fun addInfoNotifications( notifications: MutableList, prevState: StakingUiState, sendingAmount: BigDecimal, actionAmount: BigDecimal, feeValue: BigDecimal, + tonBalanceExtraFeeThreshold: BigDecimal, ) = with(notifications) { addStakingLowBalanceNotification(prevState, actionAmount) + addTonExtraFeeInfoNotification(tonBalanceExtraFeeThreshold) when (prevState.actionType) { is StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) @@ -52,39 +55,14 @@ internal class StakingInfoNotificationsFactory( is StakingActionCommonType.Pending -> { addCardanoRestakeMinimumAmountNotification(feeValue) addPendingInfoNotifications(prevState) + addTonHaveToUnstakeAllNotification(prevState) } } } private fun MutableList.addExitInfoNotifications(prevState: StakingUiState) { - val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days - - val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId - if (cooldownPeriodDays != null) { - add( - StakingNotification.Info.Unstake( - cooldownPeriodDays = cooldownPeriodDays, - subtitleRes = if (isCosmos(cryptoCurrencyNetworkIdValue)) { - R.string.staking_notification_unstake_cosmos_text - } else { - R.string.staking_notification_unstake_text - }, - ), - ) - } - - val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data - val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances - val activeStakesCount = stakingBalances.orEmpty().filter { it.type == BalanceType.STAKED }.size - - if (isTon(cryptoCurrencyNetworkIdValue) && activeStakesCount > 1) { - add( - StakingNotification.Info.Ordinary( - title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title), - text = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_text), - ), - ) - } + addUnstakeInfoNotification() + addTonHaveToUnstakeAllNotification(prevState) } private fun MutableList.addEnterInfoNotifications( @@ -245,6 +223,67 @@ internal class StakingInfoNotificationsFactory( } } + private fun MutableList.addUnstakeInfoNotification() { + val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days + + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId + if (cooldownPeriodDays != null) { + add( + StakingNotification.Info.Unstake( + cooldownPeriodDays = cooldownPeriodDays, + subtitleRes = if (isCosmos(cryptoCurrencyNetworkIdValue)) { + R.string.staking_notification_unstake_cosmos_text + } else { + R.string.staking_notification_unstake_text + }, + ), + ) + } + } + + private fun MutableList.addTonHaveToUnstakeAllNotification(prevState: StakingUiState) { + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId + + if (isTon(cryptoCurrencyNetworkIdValue)) { + val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data + val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances + + val validatorAddress = prevState.balanceState?.validator?.address ?: return + + val stakesCountWithCertainValidator = stakingBalances.orEmpty() + .filter { + it.type == BalanceType.STAKED || + it.type == BalanceType.PREPARING || + it.type == BalanceType.UNSTAKED + } + .filter { it.validator?.address == validatorAddress } + .size + + if (stakesCountWithCertainValidator > 1) { + add( + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title), + text = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_text), + ), + ) + } + } + } + + private fun MutableList.addTonExtraFeeInfoNotification(tonBalanceExtraFeeThreshold: BigDecimal) { + val amount = cryptoCurrencyStatusProvider().value.amount.orZero() + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId + + if (isTon(cryptoCurrencyNetworkIdValue) && amount >= tonBalanceExtraFeeThreshold) { + add( + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), + text = resourceReference(R.string.staking_notification_ton_extra_reserve_info), + ), + ) + } + } + private companion object { val MINIMUM_STAKE_BALANCE = "5".toBigDecimal() val MINIMUM_RESTAKE_BALANCE = "3".toBigDecimal() diff --git a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/DefaultStoriesComponent.kt b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/DefaultStoriesComponent.kt index 06f7b4b69f..5bc136d305 100644 --- a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/DefaultStoriesComponent.kt +++ b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/DefaultStoriesComponent.kt @@ -1,16 +1,14 @@ package com.tangem.feature.stories.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.stories.impl.model.StoriesModel import dagger.assisted.Assisted @@ -29,16 +27,7 @@ internal class DefaultStoriesComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state = model.state.collectAsStateWithLifecycle() SwapStoriesScreen(state.value) - - val rootBackgroundColor = LocalRootBackgroundColor.current - val previousColor = remember { rootBackgroundColor.value } - - DisposableEffect(Unit) { - rootBackgroundColor.value = TangemColorPalette.Black - onDispose { - rootBackgroundColor.value = previousColor - } - } + ChangeRootBackgroundColorEffect(TangemColorPalette.Black) } @AssistedFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index cc102270fd..a18b9be6e1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -148,7 +148,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status, externalTxUrl), + link = getStatusLink(externalTxUrl), statuses = statuses, ) } @@ -263,23 +263,16 @@ internal class TokenDetailsOnrampTransactionStateConverter( }, ) - private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + private fun getStatusLink(externalTxUrl: String?): ExpressLinkUM { if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(externalTxUrl) - }, - ) - } - else -> ExpressLinkUM.Empty - } + return ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + clickIntents.onGoToProviderClick(externalTxUrl) + }, + ) } private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 40ead2f6a4..5bdf57f4e7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -20,8 +20,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.features.tokendetails.impl.R -import com.tangem.lib.crypto.BlockchainUtils.isBSC -import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero @@ -146,7 +145,7 @@ internal class TokenDetailsStakingInfoConverter( private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { val blockchainId = status.currency.network.rawId val rewardBlockType = when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isStakingRewardUnavailable(blockchainId) -> RewardBlockType.RewardUnavailable stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards else -> RewardBlockType.Rewards } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 03f2a04277..82247b094a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -125,6 +125,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( onrampUpdateTransactionStatusUseCase( txId = txId, externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), status = statusModel.status, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2360c86709..d9546079df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -48,10 +48,8 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider import com.tangem.utils.coroutines.* -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -69,6 +67,7 @@ internal class WalletModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, + private val setWalletFirstTimeUsageUseCase: SetWalletFirstTimeUsageUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -95,9 +94,8 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() - private val expressStatusJobHolder = JobHolder() - private val clearNFTCacheJobHolder = JobHolder() private var needToRefreshWallet = false + private val clearNFTCacheJobHolder = JobHolder() private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -108,9 +106,11 @@ internal class WalletModel @Inject constructor( suggestToOpenMarkets() maybeMigrateNames() + maybeSetWalletFirstTimeUsage() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() + subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() clickIntents.initialize(innerWalletRouter, modelScope) @@ -122,6 +122,12 @@ internal class WalletModel @Inject constructor( } } + private fun maybeSetWalletFirstTimeUsage() { + modelScope.launch { + setWalletFirstTimeUsageUseCase() + } + } + override fun onDestroy() { super.onDestroy() @@ -226,7 +232,6 @@ internal class WalletModel @Inject constructor( walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) - subscribeToScreenBackgroundState(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) @@ -250,41 +255,38 @@ internal class WalletModel @Inject constructor( // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground - private fun subscribeToScreenBackgroundState(userWallet: UserWallet) { + private fun subscribeToScreenBackgroundState() { screenLifecycleProvider.isBackgroundState .onEach { isBackground -> - expressTxStatusTaskScheduler.cancelTask() - expressStatusJobHolder.cancel() refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() needToRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() - subscribeOnExpressTransactionsUpdates(userWallet) } - !isBackground -> subscribeOnExpressTransactionsUpdates(userWallet) } } .launchIn(modelScope) - .saveIn(expressStatusJobHolder) } private fun subscribeOnExpressTransactionsUpdates(userWallet: UserWallet) { - expressTxStatusTaskScheduler.cancelTask() - expressTxStatusTaskScheduler.scheduleTask( - modelScope, - PeriodicTask( - isDelayFirst = false, - delay = EXPRESS_STATUS_UPDATE_DELAY, - task = { - runCatching { - onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) - } - }, - onSuccess = { /* no-op */ }, - onError = { /* no-op */ }, - ), - ) + if (!userWallet.isMultiCurrency) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + modelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) + } + }, + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) + } } private fun observeAndClearNFTCacheIfNeedUseCase(selectedWallet: UserWallet) { @@ -306,9 +308,18 @@ internal class WalletModel @Inject constructor( val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return modelScope.launch { - refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { - Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") - } + awaitAll( + async { + refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { + Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + } + }, + async { + getWalletsUseCase.invokeSync() + .firstOrNull { it.walletId == wallet.walletCardState.id } + ?.let(::subscribeOnExpressTransactionsUpdates) + }, + ) }.saveIn(refreshWalletJobHolder) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index e39efc204f..000fb49bf3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -50,6 +51,7 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val onrampStatusFactory: OnrampStatusFactory, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -168,7 +170,7 @@ internal class WalletClickIntents @Inject constructor( modelScope.launch(dispatchers.main) { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) - + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, clickIntents = this@WalletClickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index b1ce634e92..e7624f4dd7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -423,6 +423,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ), ), ) + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index b0347bbc46..5eb8b03010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -76,6 +76,7 @@ internal class OnrampStatusFactory @Inject constructor( onrampUpdateTransactionStatusUseCase( txId = txId, externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), status = statusModel.status, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index ce57d2ac3a..5b487cb700 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -137,6 +137,7 @@ internal class SingleWalletOnrampTransactionConverter( private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { return when (status) { OnrampStatus.Status.Verifying, + OnrampStatus.Status.RefundInProgress, -> ExpressTransactionStateIconUM.Warning OnrampStatus.Status.Refunded, OnrampStatus.Status.Failed, @@ -157,7 +158,7 @@ internal class SingleWalletOnrampTransactionConverter( return ExpressStatusUM( title = resourceReference(R.string.common_transaction_status), - link = getStatusLink(status, externalTxUrl), + link = getStatusLink(externalTxUrl), statuses = statuses, ) } @@ -231,7 +232,11 @@ internal class SingleWalletOnrampTransactionConverter( resourceReference(R.string.express_status_bought, wrappedList(currency.name)) } }, - state = getStatusState(OnrampStatus.Status.Paid), + state = when { + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error + else -> getStatusState(OnrampStatus.Status.Paid) + }, ) private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( @@ -261,25 +266,22 @@ internal class SingleWalletOnrampTransactionConverter( ) } }, - state = getStatusState(OnrampStatus.Status.Sending), + state = when { + this == OnrampStatus.Status.RefundInProgress -> ExpressStatusItemState.Active + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Done + else -> getStatusState(OnrampStatus.Status.Sending) + }, ) - private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + private fun getStatusLink(externalTxUrl: String?): ExpressLinkUM { if (externalTxUrl == null) return ExpressLinkUM.Empty - return when (status) { - OnrampStatus.Status.Verifying, - OnrampStatus.Status.Failed, - -> { - ExpressLinkUM.Content( - icon = R.drawable.ic_arrow_top_right_24, - text = resourceReference(R.string.common_go_to_provider), - onClick = { - clickIntents.onGoToProviderClick(externalTxUrl) - }, - ) - } - else -> ExpressLinkUM.Empty - } + return ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + clickIntents.onGoToProviderClick(externalTxUrl) + }, + ) } private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 71205a6291..993226dafd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -148,6 +148,10 @@ object BlockchainUtils { return blockchain != Blockchain.Cardano } + fun isStakingRewardUnavailable(blockchainId: String): Boolean { + return isSolana(blockchainId) || isBSC(blockchainId) || isTon(blockchainId) + } + private fun getNetworkStandardName(blockchain: Blockchain): String { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"