Updated on 2026-08-14
This commit is contained in:
commit
2e3c7950c5
26 changed files with 370 additions and 73 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ONRAMP_ENABLED",
|
||||
"version": "5.24.0"
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "VISA_ONBOARDING_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -155,7 +155,6 @@ internal class MockCurrenciesRepository(
|
|||
|
||||
override fun getAllWalletsCryptoCurrencies(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
needFilterByAvailable: Boolean,
|
||||
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
169
mock_resources/config_dev.json
Normal file
169
mock_resources/config_dev.json
Normal 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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue