diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt index 33c6bba4f2..e3add3c7cc 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.feedback -import com.tangem.blockchain.common.Blockchain import com.tangem.tap.common.extensions.breakLine class FeedbackDataBuilder( @@ -33,9 +32,10 @@ class FeedbackDataBuilder( builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName) builder.appendKeyValue("Derivation path", walletInfo.derivationPath) - if (walletInfo.blockchain == Blockchain.Bitcoin) { - builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey) - } + // enable later + // if (walletInfo.blockchain == Blockchain.Bitcoin) { + // builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey) + // } builder.appendKeyValue("Outputs count", walletInfo.outputsCount) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 8f065eda68..d72eb92e23 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -23,6 +23,7 @@ import com.tangem.domain.common.TwinsHelper import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.ScanTask @@ -247,7 +248,7 @@ private class ScanWalletProcessor( } private fun getWalletProductType(card: CardDTO): ProductType { - if (card.batchId == CardDTO.RING_BATCH_ID) { + if (RING_BATCH_IDS.contains(card.batchId)) { return ProductType.Ring } return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt index e2824e1022..d8c3de965b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt @@ -41,7 +41,7 @@ internal object CoinsResponseConverter : Converter> { ) }, ) - } + }.filter { it.networks.isNotEmpty() } } fun getIconUrl(id: String, imageHost: String? = null): String { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index d0f3c8269e..be796e98e7 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -74,7 +74,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E .appendQueryParameter("address", walletAddress) .appendQueryParameter("signature", signature(walletAddress)) .appendQueryParameter("fix_currency", "true") - .appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL) + .appendQueryParameter("redirect_url", ExchangeUrlBuilder.SUCCESS_URL) if (isDarkTheme) builder.appendQueryParameter("theme", "1inch") blockchain.mercuryoNetwork()?.let { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 328904eb0a..947e8f1b69 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -81,7 +81,7 @@ class MoonPayService( override fun availableForSell(currency: Currency): Boolean { val availableForSell = status?.availableForSell ?: return false - val metadata = status?.responseCurrencies?.map { it.metadata } + val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata } if (!isSellAllowed()) return false return when (currency) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountBody.kt index f359bbd961..117a16e78d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountBody.kt @@ -6,5 +6,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CreateUserNetworkAccountBody( @Json(name = "networkId") val networkId: String, - @Json(name = "walletPublicKey") val publicWalletKey: String, + @Json(name = "walletPublicKey") val walletPublicKey: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountResponse.kt index 2dcb8bfb8f..829b71dd62 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserNetworkAccountResponse.kt @@ -12,6 +12,6 @@ data class CreateUserNetworkAccountResponse( @JsonClass(generateAdapter = true) data class AccountCreated( @Json(name = "accountId") val accountId: String, - @Json(name = "publicWalletKey") val publicWalletKey: String, + @Json(name = "walletPublicKey") val walletPublicKey: String, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index ea2e3141bf..95951380ca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -53,6 +53,7 @@ fun getActiveIconRes(blockchainId: String): Int { "algorand", "algorand/test" -> R.drawable.img_algorand_22 "hedera", "hedera/test" -> R.drawable.img_hedera_22 "playa3ull" -> R.drawable.img_playa3ull_22 + "DUC" -> R.drawable.img_ducatus_22 else -> R.drawable.ic_alert_24 } } @@ -107,6 +108,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "algorand", "algorand/test" -> R.drawable.img_algorand_22 "hedera-hashgraph", "hedera/test" -> R.drawable.img_hedera_22 "playa3ull-games" -> R.drawable.img_playa3ull_22 + "ducatus" -> R.drawable.img_ducatus_22 else -> R.drawable.ic_alert_24 } } @@ -158,6 +160,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "algorand" -> R.drawable.img_algorand_22 "hedera-hashgraph" -> R.drawable.img_hedera_22 "playa3ull-games-2" -> R.drawable.img_playa3ull_22 + "ducatus" -> R.drawable.img_ducatus_22 else -> R.drawable.ic_alert_24 } } @@ -212,6 +215,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "algorand", "algorand/test" -> R.drawable.ic_algorand_22 "hedera", "hedera/test" -> R.drawable.ic_hedera_22 "playa3ull" -> R.drawable.ic_playa3ull_22 + "DUC" -> R.drawable.ic_ducatus_22 else -> R.drawable.ic_alert_24 } } @@ -266,6 +270,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "algorand", "algorand/test" -> R.drawable.ic_algorand_22 "hedera-hashgraph", "hedera/test" -> R.drawable.ic_hedera_22 "playa3ull-games" -> R.drawable.ic_playa3ull_22 + "ducatus" -> R.drawable.ic_ducatus_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index c5fc3d7442..020cc01c45 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -14,12 +14,18 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" - fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String { + fun formatCryptoAmount( + cryptoAmount: BigDecimal?, + cryptoCurrency: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - val formatter = NumberFormat.getNumberInstance().apply { + val formatter = NumberFormat.getNumberInstance(locale).apply { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 + isGroupingUsed = true roundingMode = RoundingMode.DOWN } @@ -32,8 +38,12 @@ object BigDecimalFormatter { } } - fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { - return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals) + fun formatCryptoAmount( + cryptoAmount: BigDecimal?, + cryptoCurrency: CryptoCurrency, + locale: Locale = Locale.getDefault(), + ): String { + return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale) } fun formatFiatAmount( diff --git a/core/ui/src/main/res/drawable/ic_ducatus_22.xml b/core/ui/src/main/res/drawable/ic_ducatus_22.xml new file mode 100644 index 0000000000..98aa7d17c5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_ducatus_22.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_ducatus_22.xml b/core/ui/src/main/res/drawable/img_ducatus_22.xml new file mode 100644 index 0000000000..7343d4fe33 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_ducatus_22.xml @@ -0,0 +1,20 @@ + + + + + + + + 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 c1bcd54351..6074c94609 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 @@ -138,7 +138,13 @@ internal class DefaultCurrenciesRepository( storeAndPushTokens( userWalletId = userWalletId, response = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filter { it != token }, + tokens = savedCurrencies.tokens.filterNot { + // it's better to compare by fields, to support renaming and etc + it.contractAddress == token.contractAddress && + it.networkId == token.networkId && + it.derivationPath == token.derivationPath && + it.decimals == token.decimals + }, ), ) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 8ecb812304..aca3f01cc2 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -260,5 +260,5 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, - Blockchain.Ducatus, + Blockchain.Playa3ull, ) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 2616cdf4e0..8863fe8a03 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -321,6 +321,6 @@ data class CardDTO( } companion object { - const val RING_BATCH_ID = "AC17" + val RING_BATCH_IDS = listOf("AC17", "BA01") } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 82059ca4fd..e315ffa1ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -12,7 +12,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext +import kotlin.collections.set class ApplyTokenListSortingUseCase( private val currenciesRepository: CurrenciesRepository, @@ -25,11 +27,17 @@ class ApplyTokenListSortingUseCase( isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): Either { - return withContext(dispatchers.default) { - either { + return either { + val storedCurrencies = getCurrencies(userWalletId) + val isSortingTypeChanged = checkIsCurrenciesSortedByBalance(userWalletId) != isSortedByBalance + val isGroupingTypeChanged = checkIsCurrenciesGroupedByNetwork(userWalletId) != isGroupedByNetwork + + val sortedCurrencies = sortTokens(sortedTokensIds, storedCurrencies) + + if (storedCurrencies != sortedCurrencies || isSortingTypeChanged || isGroupingTypeChanged) { applySorting( userWalletId = userWalletId, - tokens = sortTokens(sortedTokensIds, getCurrencies(userWalletId)), + currencies = sortedCurrencies, isGrouped = isGroupedByNetwork, isSortedByBalance = isSortedByBalance, ) @@ -37,17 +45,29 @@ class ApplyTokenListSortingUseCase( } } + private suspend fun Raise.checkIsCurrenciesSortedByBalance(userWalletId: UserWalletId) = + catch( + block = { currenciesRepository.isTokensSortedByBalance(userWalletId).firstOrNull() ?: false }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + + private suspend fun Raise.checkIsCurrenciesGroupedByNetwork(userWalletId: UserWalletId) = + catch( + block = { currenciesRepository.isTokensGrouped(userWalletId).firstOrNull() ?: false }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + private suspend fun Raise.sortTokens( - sortedTokensIds: List, - unsortedTokens: List, + sortedCurrenciesIds: List, + unsortedCurrencies: List, ): List = withContext(dispatchers.default) { - val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) { + val nonEmptySortedTokensIds = ensureNotNull(sortedCurrenciesIds.toNonEmptySetOrNull()) { TokenListSortingError.TokenListIsEmpty } val sortedTokens = sortedMapOf() - unsortedTokens.distinct().forEach { currency -> + unsortedCurrencies.distinct().forEach { currency -> val index = nonEmptySortedTokensIds.indexOfFirst { currencyId -> currencyId == currency.id } @@ -79,12 +99,12 @@ class ApplyTokenListSortingUseCase( private suspend fun Raise.applySorting( userWalletId: UserWalletId, - tokens: List, + currencies: List, isGrouped: Boolean, isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { catch( - block = { currenciesRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, + block = { currenciesRepository.saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) }, catch = { raise(TokenListSortingError.DataError(it)) }, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index a7c421e527..1c2d68a7a9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -54,7 +54,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokensIds = MockTokens.tokens.map { it.id }, + sortedTokensIds = MockTokens.tokens.map { it.id }.sortedByDescending { it.value }, isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index a3ba7da2d9..41a8b85226 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -6,20 +6,24 @@ import android.net.Uri import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage -import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer -import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningContent import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel +import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer +import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import dagger.hilt.android.AndroidEntryPoint import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -67,22 +71,18 @@ internal class QrScanningFragment : ComposeFragment() { override fun onDestroy() { super.onDestroy() - setFitSystemWindows(fit = true) cameraPermissionLauncher.unregister() cameraExecutor.shutdown() } @Composable override fun ScreenContent(modifier: Modifier) { - SystemBarsEffect { - setSystemBarsColor(Color.Transparent, darkIcons = false) - } + StatusBarTransparencyDisposable() QrScanningContent( executor = { cameraExecutor }, analyzer = { analyzer }, uiState = viewModel.uiState.collectAsStateWithLifecycle().value, ) - setFitSystemWindows(fit = false) } private fun requestCameraPermission() { @@ -96,14 +96,33 @@ internal class QrScanningFragment : ComposeFragment() { } } - private fun setFitSystemWindows(fit: Boolean) { - activity?.window?.let { - WindowCompat.setDecorFitsSystemWindows(it, fit) - } - } - companion object { fun create() = QrScanningFragment() } +} + +@Composable +private fun StatusBarTransparencyDisposable() { + val systemUiController = rememberSystemUiController() + val systemBarsColor = TangemTheme.colors.background.secondary + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(Unit) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = false, + ) + } + if (event == Lifecycle.Event.ON_STOP) { + systemUiController.setSystemBarsColor(systemBarsColor) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0bdde7ae48..1d90e85471 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -665,7 +665,9 @@ internal class SwapInteractorImpl @Inject constructor( blockchain == Blockchain.Aptos -> { Fee.Aptos( amount = feeAmount, - gasUnitPrice = fee.feeValue.toLong() / fee.gasLimit, + gasUnitPrice = fee.feeValue.divide(BigDecimal(fee.gasLimit)) + .movePointRight(Blockchain.Aptos.decimals()) + .toLong(), gasLimit = fee.gasLimit.toLong(), ) } @@ -1358,7 +1360,7 @@ internal class SwapInteractorImpl @Inject constructor( is Fee.Common -> 0 is Fee.Ethereum -> gasLimit.toInt() is Fee.VeChain -> gasLimit.toInt() - is Fee.Aptos -> amount.longValue?.div(gasUnitPrice)?.toInt() ?: 0 + is Fee.Aptos -> gasLimit.toInt() } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 0e396ca622..d139a7b75f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -1,7 +1,9 @@ package com.tangem.feature.swap.models.states +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +@Immutable sealed class ProviderState { abstract val onProviderClick: ((String) -> Unit)? @@ -26,6 +28,7 @@ sealed class ProviderState { val selectionType: SelectionType, val additionalBadge: AdditionalBadge, val percentLowerThenBest: PercentDifference = PercentDifference.Empty, + val namePrefix: PrefixType, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -39,17 +42,25 @@ sealed class ProviderState { override val onProviderClick: ((String) -> Unit)? = null, ) : ProviderState() + @Immutable sealed class AdditionalBadge { object BestTrade : AdditionalBadge() object Empty : AdditionalBadge() object PermissionRequired : AdditionalBadge() } + @Immutable enum class SelectionType { NONE, CLICK, SELECT } + + @Immutable + enum class PrefixType { + NONE, PROVIDED_BY + } } +@Immutable sealed class PercentDifference { data class Value(val value: Float) : PercentDifference() object Empty : PercentDifference() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 301815f572..4015137fcd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -116,6 +116,7 @@ private fun ChooseProviderBottomSheet_Preview() { additionalBadge = ProviderState.AdditionalBadge.BestTrade, percentLowerThenBest = PercentDifference.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = {}, ), ProviderState.Unavailable( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 34e7da0b20..6ad6ced2fe 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -17,11 +17,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -110,12 +111,14 @@ private fun ProviderContentState( modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) { Row { - Text( - text = stringResource(id = R.string.express_by_provider), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - ) + if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) { + Text( + text = stringResource(id = R.string.express_by_provider), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), + ) + } AnimatedContent(targetState = state.name, label = "") { Text( text = it, @@ -390,75 +393,76 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { } } -@Preview +// region Preview +@Preview(showBackground = true, widthDp = 360) @Composable -private fun ProviderItem_Loading_Preview() { - Column { - TangemTheme(isDark = false) { - ProviderItemBlock(state = ProviderState.Loading()) - } - - SpacerH24() - - TangemTheme(isDark = true) { - ProviderItemBlock(state = ProviderState.Loading()) - } +private fun ProviderItemPreview_Light( + @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, +) { + TangemTheme { + ProviderItem( + modifier = Modifier.background(TangemTheme.colors.background.action), + state = state.first, + isSelected = state.second, + ) } } -@Preview +@Preview(showBackground = true, widthDp = 360) @Composable -private fun ProviderItem_Content_Preview() { - val state = ProviderState.Content( - id = "1", - name = "1inch", - type = "DEX", - iconUrl = "", - subtitle = stringReference("1 000 000"), - additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, - percentLowerThenBest = PercentDifference.Value(12.0f), - selectionType = ProviderState.SelectionType.SELECT, - onProviderClick = {}, - ) - Column { - TangemTheme(isDark = false) { - ProviderItemBlock(state = state) - } - - SpacerH24() - - TangemTheme(isDark = true) { - ProviderItemBlock(state = state) - } +private fun ProviderItemPreview_Dark( + @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, +) { + TangemTheme(isDark = true) { + ProviderItem( + modifier = Modifier.background(TangemTheme.colors.background.action), + state = state.first, + isSelected = state.second, + ) } } -@Preview -@Composable -private fun ProviderItem_Unavailable_Preview() { - val state = ProviderState.Unavailable( - id = "1", - name = "1inch", - type = "DEX", - iconUrl = "", - selectionType = ProviderState.SelectionType.SELECT, - alertText = stringReference("Unavailable"), - ) - Column { - TangemTheme(isDark = false) { - ProviderItemBlock(state = state) - } +private class ProviderItemParameterProvider : CollectionPreviewParameterProvider>( + collection = buildList { + val contentState = ProviderState.Content( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"), + additionalBadge = ProviderState.AdditionalBadge.Empty, + percentLowerThenBest = PercentDifference.Value(value = 12.0f), + selectionType = ProviderState.SelectionType.SELECT, + namePrefix = ProviderState.PrefixType.PROVIDED_BY, + onProviderClick = {}, + ) + val contentState2 = contentState.copy( + subtitle = stringReference(value = "1 132,46 MATIC"), + additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, + percentLowerThenBest = PercentDifference.Value(value = 5f), + ) + val unavailableState = ProviderState.Unavailable( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + alertText = stringReference(value = "Not available"), + selectionType = ProviderState.SelectionType.SELECT, + onProviderClick = {}, + ) + val loadingState = ProviderState.Loading() - SpacerH24() + add(contentState to true) + add(contentState to false) - TangemTheme(isDark = true) { - ProviderItemBlock(state = state) - } + add(contentState2 to true) + add(contentState2 to false) - SpacerH24() + add(unavailableState to true) + add(unavailableState to false) - TangemTheme(isDark = true) { - ProviderItem(state = state, isSelected = true) - } - } -} \ No newline at end of file + add(loadingState to true) + add(loadingState to false) + }, +) +// endregion Preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b10f504d1e..b71eded59b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1287,6 +1287,7 @@ internal class StateBuilder( additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, + namePrefix = ProviderState.PrefixType.PROVIDED_BY, onProviderClick = onProviderClick, ) } @@ -1318,6 +1319,7 @@ internal class StateBuilder( percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent -> PercentDifference.Value(percent) } ?: PercentDifference.Value(0f), + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, ) } @@ -1352,6 +1354,11 @@ internal class StateBuilder( subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, percentLowerThenBest = PercentDifference.Empty, + namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) { + ProviderState.PrefixType.PROVIDED_BY + } else { + ProviderState.PrefixType.NONE + }, onProviderClick = onProviderClick, ) } @@ -1377,7 +1384,7 @@ internal class StateBuilder( } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { - return "${this.formatToUIRepresentation()} ${token.symbol}" + return BigDecimalFormatter.formatCryptoAmount(value, token.symbol, token.decimals) } private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index a9e0e7b0c7..5342e0ece5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -147,9 +147,7 @@ internal class OrganizeTokensViewModel @Inject constructor( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateToHideProgress() - withContext( - dispatchers.main, - ) { router.popBackStack() } + withContext(dispatchers.main) { router.popBackStack() } }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index d9cc56d227..0dcd95aa6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -13,11 +13,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber internal typealias MaybeTokenListFlow = Flow> @@ -32,24 +35,25 @@ internal abstract class BasicTokenListSubscriber( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : WalletSubscriber() { - protected abstract fun tokenListFlow(): MaybeTokenListFlow + private val sendAnalyticsJobHolder = JobHolder() + private val onTokenListReceivedJobHolder = JobHolder() - protected open suspend fun onTokenListReceived(tokenList: TokenList) { /* no-op */ - } + protected abstract fun tokenListFlow(): MaybeTokenListFlow override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( flow = tokenListFlow() .onEach { maybeTokenList -> - val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) - - tokenListAnalyticsSender.send( - displayedUiState = displayedState, - userWallet = userWallet, - tokenList = maybeTokenList.getOrElse { return@onEach }, - ) + coroutineScope.launch { + sendTokenListAnalytics(maybeTokenList) + }.saveIn(sendAnalyticsJobHolder) } - .distinctUntilChanged(), + .distinctUntilChanged() + .onEach { maybeTokenList -> + coroutineScope.launch { + onTokenListReceived(maybeTokenList) + }.saveIn(onTokenListReceivedJobHolder) + }, flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(), transform = { maybeTokenList, maybeAppCurrency -> val tokenList = maybeTokenList.getOrElse { e -> @@ -64,11 +68,24 @@ internal abstract class BasicTokenListSubscriber( updateContent(tokenList, appCurrency) walletWithFundsChecker.check(tokenList) - onTokenListReceived(tokenList) }, ) } + protected open suspend fun onTokenListReceived(maybeTokenList: Either) { + /* no-op */ + } + + private suspend fun sendTokenListAnalytics(maybeTokenList: Either) { + val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) + + tokenListAnalyticsSender.send( + displayedUiState = displayedState, + userWallet = userWallet, + tokenList = maybeTokenList.getOrElse { return }, + ) + } + private fun updateContent(tokenList: TokenList, appCurrency: AppCurrency) { stateHolder.update( SetTokenListTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 8eb0af9ab2..1c53ebee84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -1,8 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import arrow.core.Either +import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -31,28 +35,34 @@ internal class MultiWalletTokenListSubscriber( override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId) - override suspend fun onTokenListReceived(tokenList: TokenList) { - updateSortingIfNeeded(tokenList) + override suspend fun onTokenListReceived(maybeTokenList: Either) { + updateSortingIfNeeded(maybeTokenList) } - private suspend fun updateSortingIfNeeded(tokenList: TokenList) { - if (tokenList.totalFiatBalance is TokenList.FiatBalance.Loading || - tokenList.sortedBy == TokenList.SortType.NONE - ) { - return - } + private suspend fun updateSortingIfNeeded(maybeTokenList: Either) { + val tokenList = maybeTokenList.getOrElse { return } + if (!checkNeedSorting(tokenList)) return applyTokenListSortingUseCase( userWalletId = userWallet.walletId, - sortedTokensIds = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { group -> - group.currencies.map { it.currency.id } - } - is TokenList.Ungrouped -> tokenList.currencies.map { it.currency.id } - is TokenList.Empty -> return - }, + sortedTokensIds = getCurrenciesIds(tokenList), isGroupedByNetwork = tokenList is TokenList.GroupedByNetwork, - isSortedByBalance = true, + isSortedByBalance = tokenList.sortedBy == TokenList.SortType.BALANCE, ) } + + private fun checkNeedSorting(tokenList: TokenList): Boolean { + return tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading && + tokenList.sortedBy == TokenList.SortType.BALANCE + } + + private fun getCurrenciesIds(tokenList: TokenList): List { + return when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { group -> + group.currencies.map { it.currency.id } + } + is TokenList.Ungrouped -> tokenList.currencies.map { it.currency.id } + is TokenList.Empty -> emptyList() + } + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5b36453cde..4c493dcd5b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ web3j = "4.10.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-503" +tangemBlockchainSdk = "develop-505" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-324" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^