Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-29 17:51:43 +03:00
commit 2e3c7950c5
26 changed files with 370 additions and 73 deletions

View file

@ -6,6 +6,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
@ -42,16 +43,23 @@ internal object UserWalletsListManagerModule {
@ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): UserWalletsListManager {
return GeneralUserWalletsListManager(
runtimeUserWalletsListManager = RuntimeUserWalletsListManager(),
biometricUserWalletsListManager = createBiometricUserWalletsListManager(applicationContext),
biometricUserWalletsListManager = createBiometricUserWalletsListManager(
applicationContext,
analyticsEventHandler,
),
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
private fun createBiometricUserWalletsListManager(applicationContext: Context): UserWalletsListManager {
private fun createBiometricUserWalletsListManager(
applicationContext: Context,
analyticsEventHandler: AnalyticsEventHandler,
): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
@ -88,6 +96,7 @@ internal object UserWalletsListManagerModule {
moshi = moshi,
secureStorage = secureStorage,
authenticatedStorage = authenticatedStorage,
analyticsEventHandler = analyticsEventHandler,
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(

View file

@ -0,0 +1,39 @@
package com.tangem.tap.domain.userWalletList.repository.implementation
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.utils.converter.Converter
object BiometricFailReasonConverter : Converter<TangemError, Basic.BiometryFailed.BiometricFailReason> {
override fun convert(value: TangemError): Basic.BiometryFailed.BiometricFailReason {
// 1. Try handle TangemSdkError cases first if error has not been mapped
when (value) {
is TangemSdkError.AuthenticationCanceled ->
return Basic.BiometryFailed.BiometricFailReason.AuthenticationCancelled
is TangemSdkError.AuthenticationAlreadyInProgress ->
return Basic.BiometryFailed.BiometricFailReason.AuthenticationAlreadyInProgress
}
// 2. For other errors, check if they are of type UserWalletsListError
if (value !is UserWalletsListError) {
return Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
}
// 3. Map UserWalletsListError to BiometricFailReason
return when (value) {
UserWalletsListError.AllKeysInvalidated ->
Basic.BiometryFailed.BiometricFailReason.AllKeysInvalidated
UserWalletsListError.BiometricsAuthenticationDisabled ->
Basic.BiometryFailed.BiometricFailReason.BiometricsAuthenticationDisabled
is UserWalletsListError.BiometricsAuthenticationLockout ->
if (value.isPermanent) {
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockoutPermanent
} else {
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockout
}
else -> Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
}
}
}

View file

@ -7,6 +7,9 @@ import com.tangem.common.*
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
@ -19,6 +22,7 @@ internal class BiometricUserWalletsKeysRepository(
moshi: Moshi,
private val authenticatedStorage: AuthenticatedStorage,
private val secureStorage: SecureStorage,
private val analyticsEventHandler: AnalyticsEventHandler,
) : UserWalletsKeysRepository {
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
@ -32,7 +36,7 @@ internal class BiometricUserWalletsKeysRepository(
return withContext(Dispatchers.IO) {
getAllInternal()
.mapFailure { error ->
when (error) {
val mappedError = when (error) {
is TangemSdkError.AuthenticationLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false)
is TangemSdkError.AuthenticationPermanentLockout ->
@ -43,6 +47,13 @@ internal class BiometricUserWalletsKeysRepository(
UserWalletsListError.BiometricsAuthenticationDisabled
else -> error
}
analyticsEventHandler.send(
Basic.BiometryFailed(
source = AnalyticsParam.ScreensSources.SignIn,
reason = BiometricFailReasonConverter.convert(mappedError),
),
)
mappedError
}
}
}

View file

@ -535,21 +535,24 @@ internal class DefaultLegacyWalletConnectRepository(
)
}
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = requestData.requestId,
result = result,
),
val params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = requestData.requestId,
result = result,
),
)
Timber.i("Session request response: $params")
WalletKit.respondSessionRequest(
params = params,
onSuccess = { response ->
Timber.i("Session request responded successfully: $response")
},
onError = { error ->
Timber.e(error.throwable, "Error while responging session request")
val params = WalletConnect.RequestHandledParams(
val handledParams = WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
@ -557,7 +560,7 @@ internal class DefaultLegacyWalletConnectRepository(
errorCode = WalletConnectError.ValidationError.error,
errorDescription = error.throwable.message,
)
analyticsHandler.send(WalletConnect.SignatureRequestFailed(params))
analyticsHandler.send(WalletConnect.SignatureRequestFailed(handledParams))
},
)
}

View file

@ -78,4 +78,25 @@ sealed class Basic(
AnalyticsParam.SOURCE to source.value,
),
)
class BiometryFailed(
source: AnalyticsParam.ScreensSources,
reason: BiometricFailReason,
) : Basic(
event = "Biometry Failed",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Reason" to reason.value,
),
) {
sealed class BiometricFailReason(val value: String) {
data object AuthenticationLockout : BiometricFailReason("BiometricsAuthenticationLockout")
data object AuthenticationLockoutPermanent : BiometricFailReason("BiometricsAuthenticationLockoutPermanent")
data object BiometricsAuthenticationDisabled : BiometricFailReason("BiometricsAuthenticationDisabled")
data object AllKeysInvalidated : BiometricFailReason("AllKeysInvalidated")
data object AuthenticationCancelled : BiometricFailReason("AuthenticationCancelled")
data object AuthenticationAlreadyInProgress : BiometricFailReason("AuthenticationAlreadyInProgress")
data class Other(val reason: String) : BiometricFailReason(reason)
}
}
}

View file

@ -5,7 +5,7 @@
},
{
"name": "ONRAMP_ENABLED",
"version": "5.24.0"
"version": "undefined"
},
{
"name": "VISA_ONBOARDING_ENABLED",

View file

@ -2,6 +2,7 @@ package com.tangem.data.managetokens.utils
import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
import com.tangem.blockchain.blockchains.sui.SuiTokenAddressConverter
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.models.network.Network
@ -9,12 +10,16 @@ import com.tangem.domain.models.network.Network
internal class TokenAddressesConverter {
private val hederaTokenAddressConverter = HederaTokenAddressConverter()
private val cardanoTokenAddressConverter = CardanoTokenAddressConverter()
private val suiTokenAddressConverter = SuiTokenAddressConverter()
fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String {
val convertedAddress = when (networkId.toBlockchain()) {
Blockchain.Hedera,
Blockchain.HederaTestnet,
-> hederaTokenAddressConverter.convertToTokenId(contractAddress)
Blockchain.Sui,
Blockchain.SuiTestnet,
-> suiTokenAddressConverter.normalizeAddress(contractAddress)
Blockchain.Cardano -> {
// TODO: [REDACTED_JIRA]
cardanoTokenAddressConverter.convertToFingerprint(contractAddress, symbol)

View file

@ -19,7 +19,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
@ -32,7 +31,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.filterIf
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ -47,9 +45,9 @@ internal class DefaultCurrenciesRepository(
private val appPreferencesStore: AppPreferencesStore,
private val expressServiceLoader: ExpressServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
private val excludedBlockchains: ExcludedBlockchains,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val userTokensSaver: UserTokensSaver,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
@ -497,14 +495,12 @@ internal class DefaultCurrenciesRepository(
@OptIn(ExperimentalCoroutinesApi::class)
override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) }
val userWalletsWithCurrencies = userWallets
.filterNot(UserWallet::isLocked)
.filterIf(needFilterByAvailable) { it.filterWalletByAvailableBlockchain(currencyRawId) }
.map { userWallet ->
if (userWallet.isMultiCurrency) {
getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
@ -529,8 +525,7 @@ internal class DefaultCurrenciesRepository(
emit(currencies)
}
}
.map { userWallet to it }
}.map { userWallet to it }
}
combine(userWalletsWithCurrencies) { it.toMap() }
@ -538,15 +533,6 @@ internal class DefaultCurrenciesRepository(
}
}
private fun UserWallet.filterWalletByAvailableBlockchain(currencyRawId: CryptoCurrency.RawID): Boolean {
val blockchain = Blockchain.fromNetworkId(currencyRawId.value) ?: return true
return this.scanResponse.card.canHandleBlockchain(
blockchain = blockchain,
cardTypesResolver = this.cardTypesResolver,
excludedBlockchains = excludedBlockchains,
)
}
override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return blockchain?.isNetworkFeeZero() ?: false
@ -557,9 +543,13 @@ internal class DefaultCurrenciesRepository(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
userTokensSaver.push(userWalletId, savedCurrencies, onFailSend = {
throw IllegalStateException("Unable to push tokens")
},)
userTokensSaver.push(
userWalletId,
savedCurrencies,
onFailSend = {
throw IllegalStateException("Unable to push tokens")
},
)
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {

View file

@ -35,9 +35,8 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase(
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean = false,
): Flow<Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>> {
return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId, needFilterByAvailable)
return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId)
.flatMapLatest { userWalletsWithCurrencies: Map<UserWallet, List<CryptoCurrency>> ->
val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) ->
val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency ->

View file

@ -249,10 +249,7 @@ interface CurrenciesRepository {
): CryptoCurrency.Token
/** Get crypto currencies by [currencyRawId] from all user wallets */
fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean,
): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean

View file

@ -155,7 +155,6 @@ internal class MockCurrenciesRepository(
override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return emptyFlow()
}

View file

@ -5,6 +5,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
@ -29,6 +31,7 @@ import javax.inject.Inject
[REDACTED_AUTHOR]
*/
internal class PortfolioDataLoader @Inject constructor(
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -38,14 +41,18 @@ internal class PortfolioDataLoader @Inject constructor(
/** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
fun load(
currencyRawId: CryptoCurrency.RawID,
availableNetworksFlow: Flow<Set<TokenMarketInfo.Network>?>,
): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
flow4 = availableNetworksFlow.filterNotNull().distinctUntilChanged(),
) { walletsWithCurrencies, appCurrency, isBalanceHidden, availableNetworks ->
PortfolioData(
walletsWithCurrencies = walletsWithCurrencies,
walletsWithCurrencies = walletsWithCurrencies.filterWalletsByAvailableNetworks(availableNetworks),
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
walletsWithBalance = emptyMap(),
@ -65,7 +72,7 @@ internal class PortfolioDataLoader @Inject constructor(
private fun getAllWalletsCryptoCurrenciesData(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId, true)
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged()
.map { walletsWithMaybeStatuses ->
walletsWithMaybeStatuses.mapValues { entry ->
@ -145,4 +152,13 @@ internal class PortfolioDataLoader @Inject constructor(
.distinctUntilChanged()
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
}
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.filterWalletsByAvailableNetworks(
availableNetworks: Set<TokenMarketInfo.Network>,
): Map<UserWallet, List<PortfolioData.CryptoCurrencyData>> {
return filter {
val networksSupportedByWallet = filterAvailableNetworksForWalletUseCase(it.key.walletId, availableNetworks)
networksSupportedByWallet.isNotEmpty()
}
}
}

View file

@ -18,7 +18,7 @@ internal typealias WalletsWithNetworks = Map<UserWalletId, Set<TokenMarketInfo.N
*/
internal class AddToPortfolioManager @Inject constructor() {
private val availableNetworks = MutableStateFlow<Set<TokenMarketInfo.Network>?>(value = null)
val availableNetworks = MutableStateFlow<Set<TokenMarketInfo.Network>?>(value = null)
private val addedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
private val removedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())

View file

@ -18,7 +18,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.wallets.models.UserWalletId
@ -50,7 +49,6 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioDataLoader: PortfolioDataLoader,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val filterAvailableNetworks: FilterAvailableNetworksForWalletUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
@ -163,7 +161,7 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun subscribeOnStateUpdates() {
combine(
flow = portfolioDataLoader.load(params.token.id),
flow = portfolioDataLoader.load(params.token.id, addToPortfolioManager.availableNetworks),
flow2 = getPortfolioUIDataFlow(),
transform = factory::create,
)
@ -177,16 +175,10 @@ internal class MarketsPortfolioModel @Inject constructor(
flow2 = selectedMultiWalletIdFlow,
flow3 = addToPortfolioManager.getAddToPortfolioData(),
transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData ->
val filteredNetworks = selectedWalletId?.let {
filterAvailableNetworks(selectedWalletId, addToPortfolioData.availableNetworks ?: emptySet())
} ?: emptySet()
PortfolioUIData(
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
selectedWalletId = selectedWalletId,
addToPortfolioData = addToPortfolioData.copy(
availableNetworks = filteredNetworks,
),
addToPortfolioData = addToPortfolioData,
hasMissedDerivations = hasMissedDerivations(selectedWalletId, addToPortfolioData),
)
},

View file

@ -157,8 +157,13 @@ private fun TokenSubtitle(
verticalAlignment = Alignment.CenterVertically,
) {
TokenRatingPlace(ratingPosition = ratingPosition)
SpacerW4()
TokenMarketCapText(text = marketCap ?: "")
if (marketCap != null) {
SpacerW4()
TokenMarketCapText(
modifier = Modifier.weight(1f, fill = false),
text = marketCap,
)
}
if (stakingRate != null) {
SpacerW4()
StakingRate(stakingRate = stakingRate.resolveReference())
@ -210,9 +215,9 @@ private fun RowScope.StakingRate(stakingRate: String) {
}
@Composable
private fun RowScope.TokenMarketCapText(text: String) {
private fun RowScope.TokenMarketCapText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = Modifier.alignByBaseline(),
modifier = modifier.alignByBaseline(),
text = text,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
@ -249,7 +254,7 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD
// region preview
@Preview(showBackground = true, widthDp = 360, name = "normal")
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 320, name = "small width")
@Preview(showBackground = true, widthDp = 260, name = "small width")
@Composable
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
TangemThemePreview {

View file

@ -116,7 +116,7 @@ internal class OnrampAmountStateFactory(
),
)
val bestProvider = selectedQuote as? OnrampQuote.Data
val bestProvider = quotes.firstOrNull()
val isMultipleQuotes = !quotes.isSingleItem()
val isOtherQuotesHasData = quotes
.filter { it.paymentMethod == selectedQuote.paymentMethod }

View file

@ -18,7 +18,7 @@ internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier)
modifier = modifier
.size(TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8)
.background(TangemColorPalette.Light1) // Ignore themed color.
.background(TangemColorPalette.Light1)
.padding(TangemTheme.dimens.spacing6),
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)

View file

@ -11,8 +11,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onramp.impl.R
@ -26,7 +26,7 @@ internal fun SelectPaymentMethodBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PaymentMethodsBottomSheetConfig>(
config = config,
addBottomInsets = true,
containerColor = TangemTheme.colors.background.tertiary,
containerColor = TangemTheme.colors.background.primary,
titleText = resourceReference(R.string.onramp_pay_with),
content = { contentConfig ->
SelectPaymentMethodBottomSheetContent(

View file

@ -95,7 +95,7 @@ internal class SendConfirmationNotificationsTransformer(
)
}
val fiatFee = formatFooterFiatFee(
amount = fee.amount,
amount = fee.amount.copy(value = fiatFeeValue),
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
appCurrency = appCurrency,

View file

@ -17,6 +17,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isTon
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
@ -65,7 +66,7 @@ internal class BalanceItemConverter(
),
rawCurrencyId = value.rawCurrencyId,
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.type.isClickable() && !value.isPending,
isClickable = value.isClickable(),
isPending = value.isPending,
)
}
@ -151,6 +152,17 @@ internal class BalanceItemConverter(
}
}
private fun BalanceItem.isClickable(): Boolean {
val networkId = cryptoCurrencyStatus.currency.network.rawId
return when {
// TON allows withdrawing funds in the preparing state, unlike other networks.
isTon(networkId) && this.type == BalanceType.PREPARING -> {
pendingActions.any { it.type == StakingActionType.WITHDRAW }
}
else -> this.type.isClickable() && !this.isPending
}
}
private fun Calendar.resetHours() {
this[Calendar.HOUR_OF_DAY] = 0
this[Calendar.MINUTE] = 0

View file

@ -750,6 +750,9 @@ internal class StateBuilder(
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
swapButton = uiState.swapButton.copy(
enabled = false,
),
permissionState = GiveTxPermissionState.InProgress,
notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications),
)

View file

@ -23,6 +23,7 @@ import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
import com.tangem.features.txhistory.utils.TxHistoryListManager
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -76,6 +77,9 @@ internal class TxHistoryModel @Inject constructor(
txHistoryListManager.uiItems
.onEach { updateState(it) }
.launchIn(modelScope)
txHistoryListManager.paginationStatus
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
.launchIn(modelScope)
}
private fun subscribeToUpdateListener() {
@ -136,14 +140,24 @@ internal class TxHistoryModel @Inject constructor(
}
}
private fun handlePaginationStatus(status: PaginationStatus<*>) {
_uiState.update { state ->
when (status) {
is PaginationStatus.InitialLoadingError -> getErrorState(state.isBalanceHidden)
PaginationStatus.EndOfPagination,
PaginationStatus.InitialLoading,
PaginationStatus.NextBatchLoading,
PaginationStatus.None,
is PaginationStatus.Paginating<*>,
-> state
}
}
}
private fun handleErrorState(error: TxHistoryStateError) {
_uiState.update { state ->
when (error) {
is TxHistoryStateError.DataError -> TxHistoryUM.Error(
isBalanceHidden = state.isBalanceHidden,
onReloadClick = ::reload,
onExploreClick = ::openExplorer,
)
is TxHistoryStateError.DataError -> getErrorState(isBalanceHidden = state.isBalanceHidden)
TxHistoryStateError.EmptyTxHistories -> TxHistoryUM.Empty(
isBalanceHidden = state.isBalanceHidden,
onExploreClick = ::openExplorer,
@ -157,6 +171,14 @@ internal class TxHistoryModel @Inject constructor(
}
}
private fun getErrorState(isBalanceHidden: Boolean): TxHistoryUM.Error {
return TxHistoryUM.Error(
isBalanceHidden = isBalanceHidden,
onReloadClick = ::reload,
onExploreClick = ::openExplorer,
)
}
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
}

View file

@ -44,6 +44,7 @@ internal class TxHistoryListManager(
)
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun init() = coroutineScope {
val batchFlow = repository.getTxHistoryBatchFlow(

View file

@ -22,7 +22,11 @@ internal class TxHistoryUiManager(
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
// filter initial states, since we dont emit loading items as UI items
.filter { it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading }
.filter {
it.status !is PaginationStatus.None &&
it.status !is PaginationStatus.InitialLoading &&
it.status !is PaginationStatus.InitialLoadingError
}
.mapLatest { state ->
state.uiBatches.asSequence()
.flatMap { it.data }

View file

@ -5,9 +5,9 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1067"
tangemBlockchainSdk = "develop-1074"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-472"
tangemCardSdk = "develop-475"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem12"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -0,0 +1,169 @@
{
"amplitudeApiKey": "place_your_key_here_if_needed",
"appsFlyer": {
"appsFlyerDevKey": "place_your_key_here_if_needed",
"appsFlyerAppID": "place_your_key_here_if_needed"
},
"blockchairApiKeys": ["place_your_key_here_if_needed"],
"blockchairAuthorizationToken": "",
"blockcypherTokens": [
"place_your_key_here_if_needed",
"place_your_key_here_if_needed",
"place_your_key_here_if_needed"
],
"bscQuiknodeApiKey": "",
"bscQuiknodeSubdomain": "place_your_data_here",
"getBlockAccessTokens": {
"avalanche": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ethereum": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ethereumClassic": {
"jsonRpc": "place_your_key_here_if_needed"
},
"fantom": {
"jsonRpc": "place_your_key_here_if_needed"
},
"rsk": {
"jsonRpc": "place_your_key_here_if_needed"
},
"bsc": {
"jsonRpc": "place_your_key_here_if_needed"
},
"polygon": {
"jsonRpc": "place_your_key_here_if_needed"
},
"xdai": {
"jsonRpc": "place_your_key_here_if_needed"
},
"cronos": {
"jsonRpc": "place_your_key_here_if_needed"
},
"solana": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ton": {
"jsonRpc": "place_your_key_here_if_needed"
},
"tron": {
"rest": "place_your_key_here_if_needed"
},
"cosmos-hub": {
"rest": "place_your_key_here_if_needed"
},
"near": {
"jsonRpc": "place_your_key_here_if_needed"
},
"xrp": {
"jsonRpc": "place_your_key_here_if_needed"
},
"cardano": {
"rosetta": "place_your_key_here_if_needed"
},
"dogecoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"litecoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"dash": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"bitcoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"aptos": {
"rest": "place_your_key_here_if_needed"
},
"algorand": {
"rest": "place_your_key_here_if_needed"
},
"polygon-zkevm": {
"jsonRpc": "place_your_key_here_if_needed"
},
"zksync": {
"jsonRpc": "place_your_key_here_if_needed"
},
"base": {
"jsonRpc": "place_your_key_here_if_needed"
},
"blast": {
"jsonRpc": "place_your_key_here_if_needed"
},
"filecoin": {
"jsonRpc": "place_your_key_here_if_needed"
},
"arbitrum-one": {
"jsonRpc": "place_your_key_here_if_needed"
},
"bitcoinCash": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"kusama": {
"jsonRpc": "place_your_key_here_if_needed"
},
"moonbeam": {
"jsonRpc": "place_your_key_here_if_needed"
},
"optimism": {
"jsonRpc": "place_your_key_here_if_needed"
},
"polkadot": {
"jsonRpc": "place_your_key_here_if_needed"
},
"shibarium": {
"jsonRpc": "place_your_key_here_if_needed"
},
"sui": {
"jsonRpc": "place_your_key_here_if_needed"
},
"telos": {
"jsonRpc": "place_your_key_here_if_needed"
},
"tezos": {
"rest": "place_your_key_here_if_needed"
}
},
"kaspaSecondaryApiUrl": "place_your_kaspa_api_here",
"infuraProjectId": "place_your_key_here_if_needed",
"mercuryoSecret": "place_your_key_here_if_needed",
"mercuryoWidgetId": "place_your_key_here_if_needed",
"moonPayApiKey": "place_your_key_here_if_needed",
"moonPayApiSecretKey": "place_your_key_here_if_needed",
"nowNodesApiKey": "place_your_key_here_if_needed",
"tonCenterApiKey": {
"mainnet": "place_your_key_here_if_needed",
"testnet": "place_your_key_here_if_needed"
},
"quiknodeApiKey": "",
"quiknodeSubdomain": "place_your_data_here_if_needed",
"tronGridApiKey": "place_your_key_here_if_needed",
"walletConnectProjectId": "place_your_key_here_if_needed",
"chiaFireAcademyApiKey": "place_your_key_here_if_needed",
"chiaTangemApiKey": "place_your_key_here_if_needed",
"express": {
"apiKey": "place_your_key_here_if_needed",
"signVerifierPublicKey": "place_your_key_here_if_needed"
},
"devExpress": {
"apiKey": "place_your_key_here_if_needed",
"signVerifierPublicKey": "place_your_key_here_if_needed"
},
"hederaArkhiaKey": "place_your_key_here_if_needed",
"polygonScanApiKey": "place_your_key_here_if_needed",
"koinosProApiKey": "place_your_key_here_if_needed",
"stakeKitApiKey": "place_your_key_here_if_needed",
"bittensorDwellirKey": "place_your_key_here_if_needed",
"bittensorOnfinalityKey": "place_your_key_here_if_needed",
"alephiumTangemApiKey": "place_your_key_here_if_needed",
"moralisApiKey": "place_your_key_here_if_needed",
"nftScanApiKey": "place_your_key_here_if_needed",
"blockaidApiKey": "place_your_key_here_if_needed"
}