diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 3951929b3c..6fdd327d88 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt new file mode 100644 index 0000000000..89013b048e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt @@ -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 { + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index 6f7c82d446..96b486a33f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -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 = 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 } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 0ac30a5cb0..2d1295d3b6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -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)) }, ) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 8f2960f7a1..110391778a 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -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) + } + } } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 8b67dd71b2..a0cd9fd966 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -5,7 +5,7 @@ }, { "name": "ONRAMP_ENABLED", - "version": "5.24.0" + "version": "undefined" }, { "name": "VISA_ONBOARDING_ENABLED", diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt index e440bc4809..e4e55b8ca6 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt @@ -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) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 67be8ab939..13eea18e86 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -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>> { 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> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 4d3159c617..14c941a905 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -35,9 +35,8 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase( @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke( currencyRawId: CryptoCurrency.RawID, - needFilterByAvailable: Boolean = false, ): Flow>>> { - return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId, needFilterByAvailable) + return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) .flatMapLatest { userWalletsWithCurrencies: Map> -> val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 14c39c9320..bb43280673 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -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>> + fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow>> fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index f8f52b1471..cbf52f0709 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -155,7 +155,6 @@ internal class MockCurrenciesRepository( override fun getAllWalletsCryptoCurrencies( currencyRawId: CryptoCurrency.RawID, - needFilterByAvailable: Boolean, ): Flow>> { return emptyFlow() } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index 6b5a84248d..057b98ca7a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -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 { + fun load( + currencyRawId: CryptoCurrency.RawID, + availableNetworksFlow: Flow?>, + ): Flow { 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>> { - 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(partialContent = null) } } } + + private fun Map>.filterWalletsByAvailableNetworks( + availableNetworks: Set, + ): Map> { + return filter { + val networksSupportedByWallet = filterAvailableNetworksForWalletUseCase(it.key.walletId, availableNetworks) + networksSupportedByWallet.isNotEmpty() + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt index 74f85480e8..aa8c98c007 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt @@ -18,7 +18,7 @@ internal typealias WalletsWithNetworks = Map?>(value = null) + val availableNetworks = MutableStateFlow?>(value = null) private val addedNetworks = MutableStateFlow(value = emptyMap()) private val removedNetworks = MutableStateFlow(value = emptyMap()) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index e31e23a7be..aa930886b0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -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), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 4c8ed29713..01bcc834fd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -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 { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 1294cc8762..2ad9356378 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -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 } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt index 5f2290e4a5..18e7c56005 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt @@ -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) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index e58782f5a5..48ce4dfda8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -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( config = config, addBottomInsets = true, - containerColor = TangemTheme.colors.background.tertiary, + containerColor = TangemTheme.colors.background.primary, titleText = resourceReference(R.string.onramp_pay_with), content = { contentConfig -> SelectPaymentMethodBottomSheetContent( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index c623c5502f..906f76bb91 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -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, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 2868d20e40..6b747f4dbd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a455ebd4e7..6fe7b46155 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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), ) diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index 13948922c0..c91a8c7bda 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -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) } diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt index f37c217511..6f7d4a8c79 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt @@ -44,6 +44,7 @@ internal class TxHistoryListManager( ) val uiItems: Flow> = uiManager.items + val paginationStatus: Flow> = state.map { it.status }.distinctUntilChanged() suspend fun init() = coroutineScope { val batchFlow = repository.getTxHistoryBatchFlow( diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt index e7621586c1..3bf17cde84 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt @@ -22,7 +22,11 @@ internal class TxHistoryUiManager( @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = 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 } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6eef76c1e1..eb5c542811 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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 ^ diff --git a/mock_resources/config_dev.json b/mock_resources/config_dev.json new file mode 100644 index 0000000000..7bdbf5433b --- /dev/null +++ b/mock_resources/config_dev.json @@ -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" +}