Updated on 2026-08-14
This commit is contained in:
commit
3752cb060c
80 changed files with 1117 additions and 464 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ class VisaCustomerWalletApproveTask(
|
|||
hash = hashToSign,
|
||||
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
|
||||
?: targetWalletPublicKey.toDecompressedPublicKey(),
|
||||
).asRSVLegacyEVM().toHexString()
|
||||
).asRSVLegacyEVM().toHexString().lowercase()
|
||||
|
||||
scanCard(
|
||||
session = session,
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -10,4 +10,13 @@ fun <T : Any, R : Any> ApiResponse<T>.fold(onSuccess: (T) -> R, onError: (ApiRes
|
|||
is ApiResponse.Error -> onError(cause)
|
||||
is ApiResponse.Success -> onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> catchApiResponseError(onError: (ApiResponseError) -> Unit, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ApiResponseError) {
|
||||
onError(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StakingOpportunities>?,
|
||||
) {
|
||||
|
||||
@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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ReferralResponse>
|
||||
|
||||
/** Make user referral, requires [StartReferralBody] */
|
||||
@POST("referral")
|
||||
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse
|
||||
suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse<ReferralResponse>
|
||||
|
||||
@GET("quotes")
|
||||
suspend fun getQuotes(
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ private fun SecondaryPairButtons(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun CloseableIconButton(
|
||||
fun CloseableIconButton(
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.moshi)
|
||||
|
|
|
|||
|
|
@ -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<List<TokenMarketExchangesResponse.Exchange>>,
|
||||
private val maxApyStore: RuntimeStateStore<BigDecimal?>,
|
||||
) : 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<BigDecimal?> {
|
||||
return maxApyStore.get()
|
||||
}
|
||||
|
||||
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
|
||||
return catchErrorAndSendEvent(block, ::createListErrorEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.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 <T> catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ApiResponseError) {
|
||||
onError(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<TokenMarketListResponse, List<TokenMarket>> {
|
||||
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, TokenMarketListWithMaxApy> {
|
||||
|
||||
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
|
||||
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<TokenMarketListResponse, Li
|
|||
monthChangePercent = token.priceChangePercentage?.day30?.movePointLeft(2),
|
||||
),
|
||||
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
|
||||
stakingRate = stakingRate,
|
||||
)
|
||||
}
|
||||
return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ internal object MarketsDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMarketsRepository(
|
||||
fun provideMarketsTokenRepository(
|
||||
marketsApi: TangemTechMarketsApi,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
|
|
@ -40,6 +40,7 @@ internal object MarketsDataModule {
|
|||
cacheRegistry = cacheRegistry,
|
||||
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
maxApyStore = RuntimeStateStore(defaultValue = null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -63,14 +63,19 @@ internal class DefaultOnrampTransactionRepository(
|
|||
}.map(transactionConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun updateTransactionStatus(txId: String, externalTxUrl: String, status: OnrampStatus.Status) =
|
||||
withContext(dispatchers.io) {
|
||||
val updatedTx = getTransactionById(txId)?.copy(
|
||||
externalTxUrl = externalTxUrl,
|
||||
status = status,
|
||||
) ?: return@withContext
|
||||
storeTransaction(updatedTx)
|
||||
}
|
||||
override suspend fun updateTransactionStatus(
|
||||
txId: String,
|
||||
externalTxId: String,
|
||||
externalTxUrl: String,
|
||||
status: OnrampStatus.Status,
|
||||
) = withContext(dispatchers.io) {
|
||||
val updatedTx = getTransactionById(txId)?.copy(
|
||||
externalTxUrl = externalTxUrl,
|
||||
externalTxId = externalTxId,
|
||||
status = status,
|
||||
) ?: return@withContext
|
||||
storeTransaction(updatedTx)
|
||||
}
|
||||
|
||||
override suspend fun removeTransaction(txId: String) {
|
||||
withContext(dispatchers.io) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,20 @@ internal class DefaultPromoRepository(
|
|||
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
|
||||
}
|
||||
|
||||
override suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean> {
|
||||
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<StoryContent?> = isReadyToShowStories(id).mapLatest {
|
||||
getStoryByIdSync(id = id, refresh = false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ data class TokenMarket(
|
|||
val isUnderMarketCapLimit: Boolean,
|
||||
val tokenQuotesShort: TokenQuotesShort,
|
||||
val tokenCharts: Charts,
|
||||
val stakingRate: BigDecimal?,
|
||||
private val imageHost: String,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ data class TokenMarketListConfig(
|
|||
) {
|
||||
|
||||
enum class Order {
|
||||
ByRating, Trending, Buyers, TopGainers, TopLosers
|
||||
ByRating, Trending, Buyers, TopGainers, TopLosers, Staking
|
||||
}
|
||||
|
||||
enum class Interval {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TokenMarketListWithMaxApy(
|
||||
val tokens: List<TokenMarket>,
|
||||
val maxApy: BigDecimal?,
|
||||
)
|
||||
|
|
@ -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<BigDecimal?> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenMarketExchange>
|
||||
|
||||
suspend fun getMaxApy(): Flow<BigDecimal?>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ interface OnrampTransactionRepository {
|
|||
|
||||
fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<OnrampTransaction>>
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -15,6 +15,10 @@ interface PromoRepository {
|
|||
suspend fun setNeverToShowWalletPromo(promoId: PromoId)
|
||||
|
||||
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
|
||||
|
||||
suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
|
||||
|
||||
suspend fun setMarketsStakingNotificationHideClicked()
|
||||
// endregion
|
||||
|
||||
// region Stories
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<BalanceItem>,
|
||||
processingActions: List<StakingAction>,
|
||||
stakingActions: List<StakingAction>,
|
||||
token: Token,
|
||||
): Either<StakingError, List<BalanceItem>> {
|
||||
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<BalanceItem>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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<List<CryptoCurrency.RawID>>(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<MarketsListItemUM>,
|
||||
val isInErrorState: Boolean,
|
||||
val isSearchNotFound: Boolean,
|
||||
val stakingNotificationMaxApy: BigDecimal?,
|
||||
val userCountry: Either<UserCountryError, UserCountry>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<CryptoCurrency.RawID>>,
|
||||
private val onLoadMoreUiItems: () -> Unit,
|
||||
private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> 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<MarketsListItemUM>,
|
||||
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<MarketsListItemUM>): MarketsListUM {
|
||||
private fun MarketsListUM.updateItems(
|
||||
newItems: ImmutableList<MarketsListItemUM>,
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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%"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
}
|
||||
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<OnrampSuccessComponentUM> get() = _state.asStateFlow()
|
||||
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var cryptoCurrency: CryptoCurrency by Delegates.notNull()
|
||||
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ internal data class ReferralStateHolder(
|
|||
val onParticipateClicked: () -> Unit,
|
||||
) : ReferralInfoState, ReferralInfoContentState
|
||||
|
||||
object Loading : ReferralInfoState
|
||||
data object Loading : ReferralInfoState
|
||||
}
|
||||
|
||||
data class ErrorSnackbar(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo
|
|||
iconResId = wrappedPairButtonsUM.leftIconResId!!,
|
||||
onClick = {
|
||||
singleEvent {
|
||||
wrappedPairButtonsUM.onLeftClick
|
||||
wrappedPairButtonsUM.onLeftClick()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<GetFeeError, TransactionFee> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var processingActions: List<StakingAction> = emptyList()
|
||||
private var stakingActions: List<StakingAction> = 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<NotificationUM>.addTonUnstakeNotification(actionType: StakingActionCommonType) {
|
||||
private fun MutableList<NotificationUM>.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<NotificationUM>,
|
||||
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<NotificationUM>.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<NotificationUM>.addEnterInfoNotifications(
|
||||
|
|
@ -245,6 +223,67 @@ internal class StakingInfoNotificationsFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
|
|||
onrampUpdateTransactionStatusUseCase(
|
||||
txId = txId,
|
||||
externalTxUrl = statusModel.externalTxUrl.orEmpty(),
|
||||
externalTxId = statusModel.externalTxId.orEmpty(),
|
||||
status = statusModel.status,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Unit>()
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -423,6 +423,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ internal class OnrampStatusFactory @Inject constructor(
|
|||
onrampUpdateTransactionStatusUseCase(
|
||||
txId = txId,
|
||||
externalTxUrl = statusModel.externalTxUrl.orEmpty(),
|
||||
externalTxId = statusModel.externalTxId.orEmpty(),
|
||||
status = statusModel.status,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue