From ef6918ce4ae3f2d435c5e6dd67c8daf2fa301077 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 14:49:51 +0500 Subject: [PATCH 01/33] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 1 - core/res/src/main/res/values-ja/strings.xml | 20 ++- core/res/src/main/res/values-ru/strings.xml | 11 +- core/res/src/main/res/values/strings.xml | 23 +++- .../ui/components/transactions/Transaction.kt | 7 +- .../src/main/res/drawable/ic_connect_24.xml | 9 ++ .../main/res/drawable/ic_disconnect_24.xml | 15 +++ core/ui/src/main/res/drawable/ic_gear_24.xml | 14 +++ .../SdkTransactionHistoryItemConverter.kt | 2 +- .../utils/SdkTransactionTypeConverter.kt | 53 ++++++-- ...TransactionDataToTxHistoryItemConverter.kt | 29 +++-- .../supply/DefaultYieldSupplyRepository.kt | 8 +- .../src/main/assets/contract_methods.json | 12 +- .../tangem/domain/models/network/TxInfo.kt | 26 +++- ...xHistoryItemToTransactionStateConverter.kt | 110 +++++++++++++---- .../SetTxHistoryCountErrorTransformer.kt | 3 + .../converter/TxHistoryItemStateConverter.kt | 114 ++++++++++++++---- .../wallet/subscribers/TxHistorySubscriber.kt | 15 ++- .../subscribers/TxHistorySubscriberV2.kt | 15 ++- gradle/tangem_dependencies.toml | 2 +- 20 files changed, 374 insertions(+), 115 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_connect_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_disconnect_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_gear_24.xml diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 95bfe4d9e6..58da791e4e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -583,7 +583,6 @@ Zuerst die Sicherung abschließen Unvollständig Andere Methoden - Physische Geräte, die Deine privaten Schlüssel sicher offline speichern. Wiederherstellungs-Phrase Um Deine Wallet auf Hardware umzustellen, erstelle vorher ein Backup. Deine privaten Schlüssel sind sicher verschlüsselt und auf Deinem Telefon gespeichert. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b15a7f50b0..3979991fb5 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -3,6 +3,10 @@ アクセスコードがないとウォレットは安全ではありません。 とにかくスキップ アクセスコードが設定されていません + コードを変更 + アクセスコードは、ウォレットのロック解除・資産へのアクセス保護に使用されます + このまま使用 + このアクセスコードは簡単に推測される可能性があります アクセスコードを入力 アクセスコードが間違っています。あと%s回間違えると、モバイルウォレットが削除されます。 アクセスコードが間違っています。あと%s回失敗すると、アプリはロックされます。 @@ -586,7 +590,6 @@ まずバックアップを完了する まずバックアップを完了する その他の方法 - 秘密鍵をオフラインで安全に保存する物理デバイス。 リカバリーフレーズ アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 ウォレットをハードウェアにアップグレードするには、まずバックアップしてください。 @@ -773,6 +776,10 @@ 取引量 これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 + 資産を即時アクセス可能な状態に保ったまま、パワーアップさせよう。%s + 利息モードを有効にする + モバイルウォレットを作成するには、%1$sにアップデートする必要があります + モバイルウォレットを使用するには、%1$s以降が必要です すべてのニュース 最新情報を入手 お使いのデバイスではNFCが使用できません @@ -1897,6 +1904,7 @@ Tangemは、生成された利息に対して15%サービス手数料も徴収します。 ネットワーク手数料が下がるか、残高が最低必要額に達すると、資金は自動的にAaveに供給されます。 過去のリターン + 保有資産に年利%1$s%%を適用 利息モードでのトークンの承認が取り消されました。トークンを開いて再度許可してください。 トークンの承認が必要です ネットワーク接続を確認してください @@ -1911,7 +1919,9 @@ 分散型・自己管理型 このサービスを利用することにより、プロバイダー\n%1$sおよび%2$sに同意するものとします。 Aaveに接続 + 残高に %1$s%% の年利(APY)を適用 Aave %1$s%% • 変動金利 + 変動金利 Aave 平均%s 昨年のリターン @@ -1938,12 +1948,16 @@ 利息モード 利息モードの有効化 利息モード - 利息モードが有効になりました + 利息モードコントラクトのデプロイ + 利息モードを有効にする %1$sがAaveに供給されました - 利息モードが無効になりました + 利息モードを無効にする %1$sがAaveから引き出されました + 利息モードをセットアップ + 利息モードの再有効化 Aaveへの供給 %1$sがAaveに供給されました + Aaveから引き出す 自動 取引のネットワーク手数料をカバーするために、 %1$s %2$sを追加してください。 %s手数料を支払えません diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index db0fefa23f..62446f8321 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -274,7 +274,7 @@ Из Из %s Синхронизировать адреса - Начать зарабатывать + Начать Получить токен К провайдеру Перейти в токен @@ -576,7 +576,7 @@ Сначала завершите создание резервной копии Не завершено Другие способы - Физические устройства, которые надёжно хранят ваш приватный ключ офлайн. + Сохраните свою recovery-фразу вручную в надёжном месте. Держите её в секрете, чтобы защитить свои средства. Фраза восстановления Чтобы защитить ваш кошелёк с помощью кода доступа, сначала завершите резервное копирование. Чтобы улучшить кошелёк до аппаратного, сначала создайте резервную копию. @@ -1075,7 +1075,7 @@ Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта или кольцо Сканировать Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку. - Приготовьте свой Tangem! + Приготовьте устройство Tangem! Уведомление безопасности Нет Да @@ -1538,6 +1538,7 @@ Восстановите существующий кошелек через Google Drive. Импорт из Google Drive Добавить существующий кошелек + Физические устройства, которые безопасно хранят ваш приватный ключ офлайн. Сканировать кошелек Tangem Импортируйте существующий кошелек через вашу фразу восстановления. Имрортировать кошелек @@ -1854,9 +1855,9 @@ Режим доходности Включение режима доходности Режим доходности - Режим доходности включен + Включение режима доходности %1$s отправлено в Aave - Режим доходности выключен + Отключение режима доходности %1$s выведено из Aave Перевод средств в Aave %1$s отправлено в Aave diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index deea678229..1dd7761587 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -3,6 +3,10 @@ Without an access code, your wallet is not secure. Skip anyway Access code not set + Change code + Your access code will be used to unlock your wallet and to protect access to the assets + Use anyway + This access code can be easily guessed Enter access code Wrong access code. Your mobile wallet will be deleted after %s more incorrect attempts. Wrong access code. The app will be locked after %s more failed attempts @@ -596,7 +600,7 @@ Finalize backup first Incomplete Other methods - Physical devices that securely store your private key offline. + Manually save your recovery phrase in a secure place and keep it private to protect your funds. Recovery phrase To secure your wallet with a Access Code, complete the backup first. To upgrade your wallet to hardware, back it up first. @@ -788,6 +792,10 @@ Volume Pull this up or tap the search bar to add tokens directly from the market Add tokens + Power up your assets while supplying them with instant access. %s + Activate Yield Mode + You must update to %1$s in order to create mobile wallet + Mobile Wallet requires %1$s or later All news Stay in the loop NFC is not available on your device @@ -1450,6 +1458,8 @@ Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. + Use your crypto for real world spending. \nIt\'s a payment card unlike any other. + Get Tangem Pay It usually takes up to 15 minutes Setting up your Tangem Card Issuing your card @@ -1474,6 +1484,7 @@ Service temporarily unavailable The service is currently unreachable. Please try again later. Sync needed + Tangem Visa Card Tangem Pay is temporarily unavailable Tangem Pay Use your card or ring to restore access to your payment account @@ -1520,6 +1531,7 @@ Multiple addresses Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. Operation + for: %s from: %s to: %s validator: %s @@ -1973,6 +1985,7 @@ Tangem also takes a 15% service fee on yield generated. Your funds will be automatically supplied to Aave once network fees are lower or your balance meets the minimum required amount. Historical returns + "Enable %1$s%% APY on your balance" Approval for your token in Yield Mode has been revoked. Open the token to grant permission again. Token approval needed Check your network connection @@ -2016,12 +2029,16 @@ Yield Mode Enabling Yield Mode Yield Mode - Yield Mode enabled + Yield Mode contract deploy + Yield Mode enable %1$s supplied to Aave - Yield Mode disabled + Yield Mode disable %1$s withdrawn from Aave + Yield Mode initialize + Yield Mode reactivate Supply to Aave %1$s supplied to Aave + Withdraw from Aave Automatic Add some %1$s %2$s to cover the network fee for transactions. Unable to cover %s fee diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 53ec6c23b8..884b8bfdae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -19,10 +19,12 @@ 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 androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility +import androidx.constraintlayout.compose.atLeast import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -94,7 +96,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod bottom.linkTo(subtitleItem.top) start.linkTo(iconItem.end) end.linkTo(amountItem.start) - width = Dimension.fillToConstraints + width = Dimension.fillToConstraints.atLeast(50.dp) }, ) @@ -122,7 +124,6 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod visibility = state.isGoneIf { amount.isEmpty() } top.linkTo(parent.top) bottom.linkTo(timestampItem.top) - start.linkTo(titleItem.end) end.linkTo(parent.end) width = Dimension.fillToConstraints }, @@ -436,7 +437,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< ), TransactionState.Content( txHash = UUID.randomUUID().toString(), - amount = "0.625 USDT", + amount = "0.62521313 USDT", time = "€0.50", status = Status.Confirmed, direction = Direction.OUTGOING, diff --git a/core/ui/src/main/res/drawable/ic_connect_24.xml b/core/ui/src/main/res/drawable/ic_connect_24.xml new file mode 100644 index 0000000000..ebab5fcf69 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_connect_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_disconnect_24.xml b/core/ui/src/main/res/drawable/ic_disconnect_24.xml new file mode 100644 index 0000000000..2baf3ca1c9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_disconnect_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_gear_24.xml b/core/ui/src/main/res/drawable/ic_gear_24.xml new file mode 100644 index 0000000000..ea7f21abb2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_gear_24.xml @@ -0,0 +1,14 @@ + + + + diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 40e874e14e..1a677b9cdb 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -24,7 +24,7 @@ internal class SdkTransactionHistoryItemConverter( SdkTransactionHistoryItem.TransactionStatus.Failed -> TxInfo.TransactionStatus.Failed SdkTransactionHistoryItem.TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed }, - type = typeConverter.convert(value.type), + type = typeConverter.convert(value.type to value.destinationType.toDomain()), amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" }, ) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt index 7e674c1f59..5e9fe1dc0b 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -1,21 +1,27 @@ package com.tangem.data.walletmanager.utils import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyInitTokenCallData +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyReactivateTokenCallData import com.tangem.domain.models.network.TxInfo import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter internal class SdkTransactionTypeConverter( private val smartContractMethods: Map, -) : Converter { +) : Converter, TxInfo.TransactionType> { - override fun convert(value: TransactionType): TxInfo.TransactionType { - return when (value) { + override fun convert(value: Pair): TxInfo.TransactionType { + val (type, destination) = value + + return when (type) { is TransactionType.ContractMethod -> { - getTransactionType(methodName = smartContractMethods[value.id]?.name) + getTransactionType(methodName = smartContractMethods[type.id]?.name, type.callData, destination) } is TransactionType.ContractMethodName -> { - getTransactionType(methodName = value.name) + getTransactionType(methodName = type.name, type.callData, destination) } is TransactionType.Transfer -> { TxInfo.TransactionType.Transfer @@ -27,7 +33,7 @@ internal class SdkTransactionTypeConverter( TxInfo.TransactionType.Staking.Unstake } is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { - TxInfo.TransactionType.Staking.Vote(value.validatorAddress) + TxInfo.TransactionType.Staking.Vote(type.validatorAddress) } is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { TxInfo.TransactionType.Staking.ClaimRewards @@ -38,7 +44,12 @@ internal class SdkTransactionTypeConverter( } } - private fun getTransactionType(methodName: String?): TxInfo.TransactionType { + @Suppress("CyclomaticComplexMethod") + private fun getTransactionType( + methodName: String?, + callData: String?, + destination: TxInfo.DestinationType, + ): TxInfo.TransactionType { return when (methodName) { "transfer" -> TxInfo.TransactionType.Transfer "approve" -> TxInfo.TransactionType.Approve @@ -59,11 +70,33 @@ internal class SdkTransactionTypeConverter( "withdrawRewardsPOL", -> TxInfo.TransactionType.Staking.ClaimRewards "redelegate" -> TxInfo.TransactionType.Staking.Restake - "supplyEnter" -> TxInfo.TransactionType.YieldSupply.Enter - "supplyExit" -> TxInfo.TransactionType.YieldSupply.Exit + "yieldSend" -> TxInfo.TransactionType.YieldSupply.Send + "enterProtocolByOwner" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.Enter( + EthereumYieldSupplyEnterCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "withdrawAndDeactivate" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.Exit( + EthereumYieldSupplyExitCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "deployYieldModule" -> TxInfo.TransactionType.YieldSupply.DeployContract( + (destination as? TxInfo.DestinationType.Single)?.addressType?.address.orEmpty(), + ) + "initYieldToken" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.InitializeToken( + EthereumYieldSupplyInitTokenCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } + "reactivateToken" -> callData?.let { data -> + TxInfo.TransactionType.YieldSupply.ReactivateToken( + EthereumYieldSupplyReactivateTokenCallData.decode(data)?.tokenContractAddress.orEmpty(), + ) + } "supplyTopUp" -> TxInfo.TransactionType.YieldSupply.Topup null -> TxInfo.TransactionType.UnknownOperation else -> TxInfo.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) - } + } ?: TxInfo.TransactionType.Operation(name = methodName?.replaceFirstChar { it.titlecase() }.orEmpty()) } } \ No newline at end of file diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index dae65d3ec7..36016b121b 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -45,7 +45,7 @@ internal class TransactionDataToTxHistoryItemConverter( TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed }, - type = getTransactionType(value.extras), + type = getTransactionType(value), amount = amount, ) } @@ -99,16 +99,25 @@ internal class TransactionDataToTxHistoryItemConverter( ) } - private fun getTransactionType(extras: TransactionExtras?): TxInfo.TransactionType { - return when (extras) { + private fun getTransactionType(transactionData: TransactionData.Uncompiled?): TxInfo.TransactionType { + return when (val extras = transactionData?.extras) { is EthereumTransactionExtras -> { - when (extras.callData) { - is EthereumYieldSupplyDeployCallData, - is EthereumYieldSupplyReactivateTokenCallData, - is EthereumYieldSupplyInitTokenCallData, - is EthereumYieldSupplyEnterCallData, - -> TxInfo.TransactionType.YieldSupply.Enter - is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit + when (val callData = extras.callData) { + is EthereumYieldSupplyDeployCallData -> TxInfo.TransactionType.YieldSupply.DeployContract( + transactionData.destinationAddress, + ) + is EthereumYieldSupplyReactivateTokenCallData -> TxInfo.TransactionType.YieldSupply.ReactivateToken( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyInitTokenCallData -> TxInfo.TransactionType.YieldSupply.InitializeToken( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyEnterCallData -> TxInfo.TransactionType.YieldSupply.Enter( + callData.tokenContractAddress, + ) + is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit( + callData.tokenContractAddress, + ) is ApprovalERC20TokenCallData -> TxInfo.TransactionType.Approve else -> TxInfo.TransactionType.Transfer } diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index e652292644..384cb82f4b 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -174,10 +174,10 @@ internal class DefaultYieldSupplyRepository( null } - private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { - it.type == TxInfo.TransactionType.YieldSupply.Enter || - it.type == TxInfo.TransactionType.Approve && - (it.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress + private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { txInfo -> + txInfo.type is TxInfo.TransactionType.YieldSupply.Enter || + txInfo.type == TxInfo.TransactionType.Approve && + (txInfo.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress } private fun Set.hasYieldExitTransactions() = any { diff --git a/domain/legacy/src/main/assets/contract_methods.json b/domain/legacy/src/main/assets/contract_methods.json index 5ca94cd491..fe538d3526 100644 --- a/domain/legacy/src/main/assets/contract_methods.json +++ b/domain/legacy/src/main/assets/contract_methods.json @@ -205,32 +205,32 @@ "0xcbeda14c": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "deployYieldModule" }, "0x79be55f7": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supplyEnter" + "name": "enterProtocolByOwner" }, "0xc65e6dcf": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supplyExit" + "name": "withdrawAndDeactivate" }, "0xebd4b81c": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "initYieldToken" }, "0xc478e956": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "supply" + "name": "reactivateToken" }, "0x0779afe6": { "info": "yieldModule", "source": "https://github.com/tangem/tangem-yield-module-contracts/", - "name": "transfer" + "name": "yieldSend" }, "0xb9de6a93": { "info": "yieldModule", diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index 65467f7460..d8fd074625 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -110,14 +110,32 @@ data class TxInfo( @Serializable sealed interface YieldSupply : TransactionType { - @Serializable - data object Enter : YieldSupply + val address: String? @Serializable - data object Exit : YieldSupply + data class Enter(override val address: String) : YieldSupply @Serializable - data object Topup : YieldSupply + data class Exit(override val address: String) : YieldSupply + + @Serializable + data object Topup : YieldSupply { + override val address: String? = null + } + + @Serializable + data object Send : YieldSupply { + override val address: String? = null + } + + @Serializable + data class DeployContract(override val address: String) : YieldSupply + + @Serializable + data class ReactivateToken(override val address: String) : YieldSupply + + @Serializable + data class InitializeToken(override val address: String) : YieldSupply } @Serializable diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt index 0fc75e0d67..bc5f160b47 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt @@ -41,7 +41,9 @@ internal class TxHistoryItemToTransactionStateConverter( R.drawable.ic_close_24 } else { when (type) { - is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.YieldSupply.DeployContract, + is TransactionType.Approve, + -> R.drawable.ic_doc_24 is TransactionType.Staking.Stake, is TransactionType.Staking.Vote, is TransactionType.Staking.Restake, @@ -51,11 +53,16 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.Staking.Unstake, is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 + is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24 + is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24 + is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24 + is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, - is TransactionType.YieldSupply, is TransactionType.UnknownOperation, + TransactionType.YieldSupply.Send, + TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } @@ -66,32 +73,76 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) - is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) - is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) + is TransactionType.YieldSupply -> when (type) { + is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) + is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) + TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) + TransactionType.YieldSupply.Send -> if (isOutgoing) { + resourceReference(R.string.common_transfer) + } else { + resourceReference(R.string.yield_module_transaction_withdraw) + } + is TransactionType.YieldSupply.DeployContract -> resourceReference( + R.string + .yield_module_transaction_deploy_contract, + ) + is TransactionType.YieldSupply.InitializeToken -> resourceReference( + R.string + .yield_module_transaction_initialize, + ) + is TransactionType.YieldSupply.ReactivateToken -> resourceReference( + R.string + .yield_module_transaction_reactivate, + ) + } is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } private fun TxInfo.extractSubtitle(): TextReference { - return when (this.type) { - is TransactionType.YieldSupply.Enter -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Topup, - -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Exit -> { - val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + return when (val type = this.type) { + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) { + if (type == TransactionType.YieldSupply.Send) { + extractSubtitleByAddressType() + } else { + resourceReference( + R.string.transaction_history_transaction_for_address, + wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + ) + } + } else { + when (type) { + is TransactionType.YieldSupply.Enter -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) + } + TransactionType.YieldSupply.Topup -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) + } + is TransactionType.YieldSupply.Exit -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + } + TransactionType.YieldSupply.Send -> { + if (isOutgoing) { + extractSubtitleByAddressType() + } else { + val amount = + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + } + else -> extractSubtitleByAddressType() + } } else -> extractSubtitleByAddressType() } @@ -133,14 +184,21 @@ internal class TxHistoryItemToTransactionStateConverter( @Suppress("ComplexCondition") private fun TxInfo.getAmount(): String { - if (type is TransactionType.Staking.Vote || - type == TransactionType.Staking.ClaimRewards || - type == TransactionType.Staking.Withdraw || - type == TransactionType.YieldSupply.Enter || - type == TransactionType.YieldSupply.Exit || - type == TransactionType.YieldSupply.Topup - ) { - return "" + when (type) { + is TransactionType.Staking.Vote, + TransactionType.Staking.ClaimRewards, + TransactionType.Staking.Withdraw, + -> return "" + + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Token) { + when (type) { + TransactionType.YieldSupply.Send -> if (!isOutgoing) { + return "" + } + else -> return "" + } + } + else -> Unit } val prefix = when { status == TxInfo.TransactionStatus.Failed -> "" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index 3b5dfff91c..fa92bb7623 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.models.wallet.UserWallet @@ -17,6 +18,7 @@ internal class SetTxHistoryCountErrorTransformer( private val error: TxHistoryStateError, private val pendingTransactions: Set, private val clickIntents: WalletClickIntents, + private val currency: CryptoCurrency, ) : WalletStateTransformer(userWallet.walletId) { private val txHistoryItemConverter by lazy { @@ -26,6 +28,7 @@ internal class SetTxHistoryCountErrorTransformer( } TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index db435626e8..a30612c67e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.network.TxInfo.TransactionStatus import com.tangem.domain.models.network.TxInfo.TransactionType @@ -20,6 +21,7 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat internal class TxHistoryItemStateConverter( + private val currency: CryptoCurrency, private val symbol: String, private val decimals: Int, private val clickIntents: WalletClickIntents, @@ -49,7 +51,9 @@ internal class TxHistoryItemStateConverter( R.drawable.ic_close_24 } else { when (type) { - is TransactionType.Approve -> R.drawable.ic_doc_24 + is TransactionType.YieldSupply.DeployContract, + is TransactionType.Approve, + -> R.drawable.ic_doc_24 is TransactionType.Staking.Stake, is TransactionType.Staking.Vote, is TransactionType.Staking.Restake, @@ -59,11 +63,16 @@ internal class TxHistoryItemStateConverter( is TransactionType.Staking.Unstake, is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 + is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24 + is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24 + is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24 + is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, - is TransactionType.YieldSupply, is TransactionType.UnknownOperation, + TransactionType.YieldSupply.Send, + TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } } @@ -74,32 +83,81 @@ internal class TxHistoryItemStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) - is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) - is TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) + is TransactionType.YieldSupply -> when (type) { + is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) + is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) + TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) + TransactionType.YieldSupply.Send -> if (isOutgoing) { + resourceReference(R.string.common_transfer) + } else { + resourceReference(R.string.yield_module_transaction_withdraw) + } + is TransactionType.YieldSupply.DeployContract -> resourceReference( + R.string + .yield_module_transaction_deploy_contract, + ) + is TransactionType.YieldSupply.InitializeToken -> resourceReference( + R.string + .yield_module_transaction_initialize, + ) + is TransactionType.YieldSupply.ReactivateToken -> resourceReference( + R.string + .yield_module_transaction_reactivate, + ) + } is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } private fun TxInfo.extractSubtitle(): TextReference { - return when (this.type) { - is TransactionType.YieldSupply.Enter -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Topup, - -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount)) - } - is TransactionType.YieldSupply.Exit -> { - val amount = amount.format { crypto(symbol = symbol, decimals = decimals) } - resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) + return when (val type = this.type) { + is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) { + resourceReference( + R.string.transaction_history_transaction_for_address, + wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + ) + } else { + when (type) { + is TransactionType.YieldSupply.Enter -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_enter_subtitle, + wrappedList(amount), + ) + } + TransactionType.YieldSupply.Topup -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_topup_subtitle, + wrappedList(amount), + ) + } + TransactionType.YieldSupply.Send -> { + if (isOutgoing) { + extractSubtitleByAddressType() + } else { + val amount = + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + } + is TransactionType.YieldSupply.Exit -> { + val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } + resourceReference( + R.string.yield_module_transaction_exit_subtitle, + wrappedList(amount), + ) + } + else -> extractSubtitleByAddressType() + } } else -> extractSubtitleByAddressType() } @@ -147,14 +205,18 @@ internal class TxHistoryItemStateConverter( @Suppress("ComplexCondition") private fun TxInfo.getAmount(): String { - if (type is TransactionType.Staking.Vote || - type == TransactionType.Staking.ClaimRewards || - type == TransactionType.Staking.Withdraw || - type == TransactionType.YieldSupply.Enter || - type == TransactionType.YieldSupply.Exit || - type == TransactionType.YieldSupply.Topup - ) { - return "" + when (type) { + is TransactionType.Staking.Vote, + TransactionType.Staking.ClaimRewards, + TransactionType.Staking.Withdraw, + -> return "" + + is TransactionType.YieldSupply -> { + if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) { + return "" + } + } + else -> Unit } val prefix = when { status == TransactionStatus.Failed -> "" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index b9c65f6340..bef7b6defb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -5,6 +5,7 @@ import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet @@ -58,7 +59,7 @@ internal class TxHistorySubscriber( refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems) + setLoadedTxHistoryState(maybeTxHistoryItems, status.currency) } } } @@ -67,18 +68,19 @@ internal class TxHistorySubscriber( private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { stateHolder.update( maybeTxHistoryItemCount.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, - error = it, + error = error, pendingTransactions = status.value.pendingTransactions, + currency = status.currency, clickIntents = clickIntents, ) }, - ifRight = { + ifRight = { txCount -> SetTxHistoryCountTransformer( userWalletId = userWallet.walletId, - transactionsCount = it, + transactionsCount = txCount, clickIntents = clickIntents, ) }, @@ -86,7 +88,7 @@ internal class TxHistorySubscriber( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { stateHolder.update( maybeTxHistoryItems.fold( ifLeft = { @@ -102,6 +104,7 @@ internal class TxHistorySubscriber( symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, + currency = currency, ) SetTxHistoryItemsTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt index 23e7c78681..78c388f87f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt @@ -5,6 +5,7 @@ import androidx.paging.cachedIn import androidx.paging.map import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet @@ -51,7 +52,7 @@ internal class TxHistorySubscriberV2( refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems) + setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) } } } @@ -60,18 +61,19 @@ internal class TxHistorySubscriberV2( private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { stateController.update( maybeTxHistoryItemCount.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, - error = it, + error = error, pendingTransactions = status.value.pendingTransactions, clickIntents = clickIntents, + currency = status.currency, ) }, - ifRight = { + ifRight = { txCount -> SetTxHistoryCountTransformer( userWalletId = userWallet.walletId, - transactionsCount = it, + transactionsCount = txCount, clickIntents = clickIntents, ) }, @@ -79,7 +81,7 @@ internal class TxHistorySubscriberV2( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { stateController.update( maybeTxHistoryItems.fold( ifLeft = { @@ -92,6 +94,7 @@ internal class TxHistorySubscriberV2( ifRight = { itemsFlow -> val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() val itemConverter = TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 07be659f64..cbabfbf53b 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31-1314" +tangemBlockchainSdk = "releases-5.31.1-1316" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.31-569" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 76c129f20552e8523f41913c63ef2bbb093e5a19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Dec 2025 15:12:38 +0500 Subject: [PATCH 02/33] Updated on 2026-08-14 --- .../usecase/YieldSupplyGetRewardsBalanceUseCase.kt | 4 ++-- .../usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt | 4 ++-- .../tokendetails/model/TokenDetailsModel.kt | 10 ++-------- .../yield/supply/impl/main/model/YieldSupplyModel.kt | 6 ++++++ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index 5e1b8676a9..52c1434981 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -92,8 +92,8 @@ class YieldSupplyGetRewardsBalanceUseCase( } private companion object { - const val TICK_MILLIS: Long = 300 - private val TICK_SECONDS_BD = BigDecimal("0.3") + const val TICK_MILLIS: Long = 800 + private val TICK_SECONDS_BD = BigDecimal("0.8") private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") // 365 * 24 * 60 * 60 private val HUNDRED_BD = BigDecimal("100") private const val SCALE = 18 diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index 254c8c6d4a..3bdd2f446e 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -288,12 +288,12 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { private companion object { private const val SCALE = 18 - private val TICK_SECONDS_BD = BigDecimal("0.3") + private val TICK_SECONDS_BD = BigDecimal("0.8") private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") private val HUNDRED_BD = BigDecimal("100") private const val MIN_DECIMALS = 3 - private const val MAX_DECIMALS = 8 + private const val MAX_DECIMALS = 12 private val LN_10 = ln(10.0) private const val EPSILON = 1e-18 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 809fa4ea3f..3ef6e97629 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -1251,14 +1251,8 @@ internal class TokenDetailsModel @Inject constructor( } private fun handleNavigationParam() { - when (val action = params.navigationAction) { - is NavigationAction.Staking -> openStaking() - is NavigationAction.YieldSupply -> if (action.isActive) { - modelScope.launch(dispatchers.default) { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id) - } - } - else -> Unit + if (params.navigationAction is NavigationAction.Staking) { + openStaking() } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 95f483022e..c8f25dea1a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -89,6 +89,12 @@ internal class YieldSupplyModel @Inject constructor( val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency) if (isAvailable) { subscribeOnCurrencyStatusUpdates() + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ), + ) } } } From a13a54accda0cff8cd488d2ecc54e583e8620244 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 13:24:16 +0500 Subject: [PATCH 03/33] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 20 ++ .../ui/tokens/TokenItemStateConverter.kt | 50 ++++- .../local/preferences/PreferencesKeys.kt | 4 + core/res/src/main/res/values-ja/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 6 +- .../core/ui/components/token/TokenItem.kt | 54 ++++- .../token/internal/YieldSupplyPromoBanner.kt | 110 +++++++++ .../components/token/state/TokenItemState.kt | 19 ++ .../core/ui/test/TokenElementsTestTags.kt | 1 + .../main/res/drawable/ic_rectangle_bottom.xml | 9 + .../tangem/utils/coroutines/CoroutineExt.kt | 8 +- data/yield-supply/build.gradle.kts | 4 + .../supply/DefaultYieldSupplyRepository.kt | 21 +- .../yield/supply/di/YieldSupplyDataModule.kt | 3 + .../yield/supply/YieldSupplyRepository.kt | 4 + .../usecase/YieldSupplyApyFlowUseCase.kt | 5 +- ...ieldSupplyGetShouldShowMainPromoUseCase.kt | 13 ++ ...ieldSupplySetShouldShowMainPromoUseCase.kt | 12 + .../intents/WalletContentClickIntents.kt | 10 + .../implementors/MultiWalletContentLoader.kt | 3 + .../MultiWalletContentLoaderFactory.kt | 3 + .../SingleWalletWithTokenContentLoader.kt | 3 + ...ngleWalletWithTokenContentLoaderFactory.kt | 3 + .../transformers/SetTokenListTransformer.kt | 5 +- .../converter/TokenListStateConverter.kt | 12 +- .../YieldSupplyPromoBannerKeyConverter.kt | 48 ++++ .../subscribers/AccountListSubscriber.kt | 14 +- .../subscribers/BasicAccountListSubscriber.kt | 21 +- .../subscribers/BasicTokenListSubscriber.kt | 16 +- .../MultiWalletTokenListSubscriber.kt | 3 + .../SingleWalletWithTokenListSubscriber.kt | 3 + .../YieldSupplyPromoBannerKeyConverterTest.kt | 208 ++++++++++++++++++ 32 files changed, 665 insertions(+), 35 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt create mode 100644 core/ui/src/main/res/drawable/ic_rectangle_bottom.xml create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt create mode 100644 features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index a151bfa469..28f00e7e98 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -199,4 +199,24 @@ internal object YieldSupplyDomainModule { yieldSupplyRepository = yieldSupplyRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyGetShouldShowMainPromoUseCase { + return YieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplySetShouldShowMainPromoUseCase { + return YieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 9817a2f88a..516a15539c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -43,12 +43,14 @@ import java.math.BigDecimal */ class TokenItemStateConverter( private val appCurrency: AppCurrency, - private val yieldModuleApyMap: Map = emptyMap(), + private val yieldModuleApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val yieldSupplyPromoBannerKey: String? = null, private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) }, private val onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)? = null, + private val onYieldPromoCloseClick: (() -> Unit)? = null, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus -> createTitleState( currencyStatus = currencyStatus, @@ -66,6 +68,15 @@ class TokenItemStateConverter( private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { createFiatAmountState(status = it, appCurrency = appCurrency) }, + private val promoBannerProvider: (CryptoCurrencyStatus) -> TokenItemState.PromoBannerState = { status -> + createPromoBannerState( + status = status, + yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey, + onApyLabelClick = onApyLabelClick, + onYieldPromoCloseClick = onYieldPromoCloseClick, + ) + }, private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, ) : Converter { @@ -102,6 +113,7 @@ class TokenItemStateConverter( subtitleState = requireNotNull(subtitleStateProvider(this)), fiatAmountState = requireNotNull(fiatAmountStateProvider(this)), subtitle2State = requireNotNull(subtitle2StateProvider(this)), + promoBannerState = promoBannerProvider(this), onItemClick = onItemClick?.let { onItemClick -> { onItemClick(it, this) } }, @@ -164,7 +176,7 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?, ): TokenItemState.TitleState { @@ -204,7 +216,7 @@ class TokenItemStateConverter( // polygon-pos_0xc2132d05d31c914a87c6611c10748aeb04b58e8f private fun resolveEarnApy( cryptoCurrencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, ): EarnApyInfo? { val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token @@ -223,7 +235,7 @@ class TokenItemStateConverter( wrappedList(yieldSupplyApy), ), isActive = isActive, - apy = yieldSupplyApy, + apy = yieldSupplyApy.toString(), ) } } @@ -379,6 +391,36 @@ class TokenItemStateConverter( } } + private fun createPromoBannerState( + status: CryptoCurrencyStatus, + yieldModuleApyMap: Map, + yieldSupplyPromoBannerKey: String?, + onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?, + onYieldPromoCloseClick: (() -> Unit)?, + ): TokenItemState.PromoBannerState { + val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty + if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() || + yieldModuleApyMap[token.yieldSupplyKey()] == null + ) { + return TokenItemState.PromoBannerState.Empty + } + val yieldSupplyApy = + yieldModuleApyMap[token.yieldSupplyKey()] ?: return TokenItemState.PromoBannerState.Empty + + return TokenItemState.PromoBannerState.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(yieldSupplyApy), + ), + onPromoBannerClick = { + onApyLabelClick?.invoke(status, yieldSupplyApy.toString()) + }, + onCloseClick = { + onYieldPromoCloseClick?.invoke() + }, + ) + } + private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 2c128557a6..f3f7fbf07d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -122,6 +122,10 @@ object PreferencesKeys { val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") } + val YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY by lazy { + booleanPreferencesKey(name = "yieldSupplyShouldShowMainPromo") + } + val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") } // region Notifications diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3979991fb5..849f728af4 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -590,6 +590,7 @@ まずバックアップを完了する まずバックアップを完了する その他の方法 + リカバリーフレーズは、ご自身で安全な場所に保管し、資金を守るために他人には絶対に共有しないでください。 リカバリーフレーズ アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 ウォレットをハードウェアにアップグレードするには、まずバックアップしてください。 @@ -1432,6 +1433,8 @@ カードの発行に失敗しました 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 技術的なエラーが発生しました。サポートへお問い合わせください。 + 暗号資産を日常の支払いに使おう。\n\nこれまでにないタイプの決済カード。 + Tangem Payを入手 通常は最大で15分ほどかかります Tangemカードのセットアップ カードを発行しています @@ -1454,6 +1457,7 @@ 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在サービスに接続できません。後ほどもう一度お試しください。 + Tangem Visaカード Tangem Payは一時的に利用できません Tangem Pay カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 @@ -1500,6 +1504,7 @@ 複数のアドレス 現在、このブロックチェーンでは取引履歴はサポートされていません。しかしご心配なく!弊社で対応中です。その間、エクスプローラーで確認することができます。 オペレーション + 対象:%s 送金元: %s 送金先: %s バリデーター: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1dd7761587..0bbcf90b6a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -527,7 +527,7 @@ Buying %s Buying %s... Hide this transaction - Once hidden, the transaction status cannot be viewed again. You can simply swipe to dismiss instead. + If you hide this transaction, it won’t appear in the status screen anymore. If you just want to close the status screen and return later, simply swipe it away instead. Hide Transaction Status? This token is not supported. Please choose a different token to swap. %s is not supported @@ -697,11 +697,13 @@ APY %s My portfolio Market + Earn with Tangem To generate addresses for selected networks, you must scan your Tangem Wallet card or ring To add tokens pull this up or tap the search bar This section’s data is sourced from the following networks: %s Unable to load the data… No data + Market Pulse Quick actions Search through the market Result @@ -1985,7 +1987,7 @@ Tangem also takes a 15% service fee on yield generated. Your funds will be automatically supplied to Aave once network fees are lower or your balance meets the minimum required amount. Historical returns - "Enable %1$s%% APY on your balance" + Enable %1$s%% APY on your balance Approval for your token in Yield Mode has been revoked. Open the token to grant permission again. Token approval needed Check your network connection diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 374b6de2e3..fc56519c01 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.currency.icon.CurrencyIcon @@ -28,6 +30,7 @@ import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State +import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -44,7 +47,7 @@ private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32 private enum class LayoutId { - ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT, PROMO_BANNER } /** @@ -109,6 +112,14 @@ fun TokenItem( .testTag(TokenElementsTestTags.TOKEN_ICON), ) + YieldSupplyPromoBanner( + state = state.promoBannerState, + modifier = Modifier + .layoutId(layoutId = LayoutId.PROMO_BANNER) + .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .fillMaxWidth(), + ) + TokenTitle( state = state.titleState, modifier = Modifier @@ -220,6 +231,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints) + val promoBanner = when (state.promoBannerState) { + is PromoBannerState.Content -> measurables.measure( + layoutId = LayoutId.PROMO_BANNER, + constraints = constraints, + ) + else -> null + } var firstRowRemainingFreeSpace: Int? = null var secondRowRemainingFreeSpace: Int? = null @@ -283,10 +301,18 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) } + val promoBannerHeight = promoBanner?.height ?: 0 + val promoOffset = if (promoBannerHeight > 0) { + promoBannerHeight - 8.dp.roundToPx() + } else { + 0 + } + val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, layoutPadding = verticalPadding, + promoOffset = promoOffset, title = title, fiatAmount = fiatAmount, cryptoAmount = cryptoAmount, @@ -294,16 +320,22 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2)) + promoBanner?.placeRelative(x = 0, y = 0) + + icon.placeRelative( + x = 0, + y = promoOffset + (layoutHeight - promoOffset - icon.height) + .div(other = 2), + ) title.placeRelative( x = icon.width, - y = when (state) { + y = promoOffset + when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { if (state.subtitleState == null) { - (layoutHeight - title.height).div(other = 2) + (layoutHeight - promoOffset - title.height).div(other = 2) } else { verticalPadding } @@ -314,8 +346,8 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier fiatAmount?.placeRelative( x = layoutWidth - fiatAmount.width, - y = when (state.subtitle2State) { - null -> (layoutHeight - fiatAmount.height).div(other = 2) + y = promoOffset + when (state.subtitle2State) { + null -> (layoutHeight - promoOffset - fiatAmount.height).div(other = 2) else -> verticalPadding }, ) @@ -335,7 +367,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier nonFiatContent.placeRelative( x = layoutWidth - nonFiatContent.width, - y = (layoutHeight - nonFiatContent.height).div(other = 2), + y = promoOffset + (layoutHeight - promoOffset - nonFiatContent.height).div(other = 2), ) } } @@ -443,6 +475,7 @@ private fun calculateLayoutHeight( state: TokenItemState, minLayoutHeight: Int, layoutPadding: Int, + promoOffset: Int, title: Placeable, fiatAmount: Placeable?, cryptoAmount: Placeable?, @@ -468,7 +501,7 @@ private fun calculateLayoutHeight( } } - return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight) + return (promoOffset + max(firstColumnHeight, secondColumnHeight)).coerceAtLeast(promoOffset + minLayoutHeight) } @Preview(widthDp = 360, showBackground = true) @@ -587,6 +620,11 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider YieldSupplyPromoBanner(state = state, modifier = modifier) + is PromoBannerState.Empty -> Unit + } +} + +@Composable +internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: Modifier = Modifier) { + val bgColor = TangemTheme.colors.control.unchecked + Column(modifier = modifier) { + Row( + modifier = Modifier + .background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 8.dp) + .clickable(onClick = state.onPromoBannerClick) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size16), + ) + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .weight(1f) + .padding(end = TangemTheme.dimens.spacing8), + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.secondary, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = { state.onCloseClick() }, + ), + ) + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rectangle_bottom), + contentDescription = null, + tint = bgColor, + modifier = Modifier + .size(width = 12.dp, height = 8.dp), + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_YieldSupplyPromoBanner() { + TangemThemePreview { + YieldSupplyPromoBanner( + state = PromoBannerState.Content( + title = TextReference.Str(value = "Earn up to 5% APY"), + onPromoBannerClick = {}, + onCloseClick = {}, + ), + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 7f05cdb7cb..a2180bb614 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -34,6 +34,8 @@ sealed class TokenItemState { */ abstract val subtitle2State: Subtitle2State? + abstract val promoBannerState: PromoBannerState + /** Callback which will be called when an item is clicked */ abstract val onItemClick: ((TokenItemState) -> Unit)? @@ -59,6 +61,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val subtitle2State: Subtitle2State = Subtitle2State.Loading + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -75,6 +78,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val subtitle2State: Subtitle2State = Subtitle2State.Locked + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -99,6 +103,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState?, override val subtitle2State: Subtitle2State?, + override val promoBannerState: PromoBannerState = PromoBannerState.Empty, override val onItemClick: ((TokenItemState) -> Unit)?, override val onItemLongClick: ((TokenItemState) -> Unit)?, override val onApyLabelClick: ((TokenItemState) -> Unit)? = null, @@ -120,6 +125,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -146,6 +152,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val subtitle2State: Subtitle2State? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } /** @@ -168,6 +175,7 @@ sealed class TokenItemState { override val subtitle2State: Subtitle2State? = null override val onItemClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } @Immutable @@ -254,4 +262,15 @@ sealed class TokenItemState { data object Locked : Subtitle2State() } + + @Immutable + sealed class PromoBannerState { + data class Content( + val title: TextReference, + val onPromoBannerClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : PromoBannerState() + + data object Empty : PromoBannerState() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt index a6c08721f7..d513f7791c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt @@ -7,4 +7,5 @@ object TokenElementsTestTags { const val TOKEN_FIAT_AMOUNT = "TOKEN_FIAT_AMOUNT" const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT" const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK" + const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml new file mode 100644 index 0000000000..6b77c79cb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 8a2fd97b72..7d14d15efb 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -69,15 +69,16 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } @Suppress("LongParameterList", "MagicNumber") -inline fun combine6( +inline fun combine7( flow1: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, flow6: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, -): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + flow7: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6, flow7) { arr -> @Suppress("UNCHECKED_CAST") transform( arr[0] as T1, @@ -86,5 +87,6 @@ inline fun combine6( arr[3] as T4, arr[4] as T5, arr[5] as T6, + arr[6] as T7, ) } \ No newline at end of file diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index 4fbb4f314a..cc7313cd67 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -19,6 +19,10 @@ dependencies { /** Tangem SDKs */ implementation(tangemDeps.blockchain) + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + /** Core */ implementation(projects.core.datasource) implementation(projects.core.utils) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 384cb82f4b..a40499ba87 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -12,6 +12,10 @@ import com.tangem.data.yield.supply.converters.YieldTokenChartConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -35,6 +39,7 @@ internal class DefaultYieldSupplyRepository( private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val appPreferencesStore: AppPreferencesStore, ) : YieldSupplyRepository { private val statusMap: MutableMap = ConcurrentHashMap() @@ -174,10 +179,18 @@ internal class DefaultYieldSupplyRepository( null } - private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { txInfo -> - txInfo.type is TxInfo.TransactionType.YieldSupply.Enter || - txInfo.type == TxInfo.TransactionType.Approve && - (txInfo.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress + override fun getShouldShowYieldPromoBanner(): Flow { + return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true) + } + + override suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) { + appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, shouldShow) + } + + private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { + it.type == TxInfo.TransactionType.YieldSupply.Enter || + it.type == TxInfo.TransactionType.Approve && + (it.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress } private fun Set.hasYieldExitTransactions() = any { diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 409d1da57f..ee040a543a 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.datasource.api.tangemTech.YieldSupplyApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyRepository @@ -41,6 +42,7 @@ internal object YieldSupplyDataModule { walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, analyticsExceptionHandler: AnalyticsExceptionHandler, + appPreferencesStore: AppPreferencesStore, ): YieldSupplyRepository { return DefaultYieldSupplyRepository( yieldSupplyApi = yieldSupplyApi, @@ -48,6 +50,7 @@ internal object YieldSupplyDataModule { dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, analyticsExceptionHandler = analyticsExceptionHandler, + appPreferencesStore = appPreferencesStore, ) } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 8ab13bbac1..9a73322ca8 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -108,4 +108,8 @@ interface YieldSupplyRepository { userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): YieldSupplyEnterStatus? + + fun getShouldShowYieldPromoBanner(): Flow + + suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index d93fcca1a1..3dda098013 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.math.BigDecimal /** * Emits a map of APY values per token. @@ -15,11 +16,11 @@ class YieldSupplyApyFlowUseCase( private val yieldSupplyRepository: YieldSupplyRepository, ) { - operator fun invoke(): Flow> { + operator fun invoke(): Flow> { return yieldSupplyRepository.getMarketsFlow() .map { yieldMarketTokenList -> yieldMarketTokenList.filter { it.isActive }.associate { token -> - token.yieldSupplyKey to token.apy.toString() + token.yieldSupplyKey to token.apy } } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..42a1493d5b --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository +import kotlinx.coroutines.flow.Flow + +class YieldSupplyGetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + operator fun invoke(): Flow { + return yieldSupplyRepository.getShouldShowYieldPromoBanner() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..dcc0a985e6 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplySetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(shouldShow: Boolean) { + yieldSupplyRepository.setShouldShowYieldPromoBanner(shouldShow) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 8c1278e5b6..0cd9d5e7b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -21,6 +21,7 @@ import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap @@ -52,6 +53,8 @@ internal interface WalletContentClickIntents { fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, apy: String) + fun onYieldPromoCloseClick() + fun onAccountExpandClick(account: Account) fun onAccountCollapseClick(account: Account) @@ -89,6 +92,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val accountDependencies: AccountDependencies, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, + private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -182,6 +186,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } + override fun onYieldPromoCloseClick() { + modelScope.launch { + yieldSupplySetShouldShowMainPromoUseCase(false) + } + } + override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 0ce2d80a8d..e08e469afc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -45,6 +46,7 @@ internal class MultiWalletContentLoader( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -63,6 +65,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) WalletNFTListSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index d3fe3d60a4..1f9d0570e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -44,6 +45,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -72,6 +74,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 7f3e9f91b6..6d895697ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -6,6 +6,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -35,6 +36,7 @@ internal class SingleWalletWithTokenContentLoader( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -49,6 +51,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 0f6ac06ef5..4d89203c74 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -7,6 +7,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -36,6 +37,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -55,6 +57,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4d102bb5d8..0dec24b4b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -11,14 +11,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber +import java.math.BigDecimal internal class SetTokenListTransformer( private val params: TokenConverterParams, private val userWallet: UserWallet, private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, - private val yieldSupplyApyMap: Map = emptyMap(), + private val yieldSupplyApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val shouldShowMainPromo: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -63,6 +65,7 @@ internal class SetTokenListTransformer( clickIntents = clickIntents, yieldModuleApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index f9eefeccd6..9037cb7b57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -28,17 +28,25 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig +@Suppress("LongParameterList") internal class TokenListStateConverter( private val appCurrency: AppCurrency, private val params: TokenConverterParams, private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, - private val yieldModuleApyMap: Map, + private val yieldModuleApyMap: Map, private val stakingApyMap: Map>, + private val shouldShowMainPromo: Boolean, ) : Converter { + private val yieldSupplyPromoBannerKeyConverter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) @@ -57,10 +65,12 @@ internal class TokenListStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params), stakingApyMap = stakingApyMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apy -> onApyLabelClick(status, apy) }, + onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, ) override fun convert(value: WalletTokensListState): WalletTokensListState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt new file mode 100644 index 0000000000..dd2569e500 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class YieldSupplyPromoBannerKeyConverter( + private val yieldModuleApyMap: Map, + private val shouldShowMainPromo: Boolean, +) : Converter { + + override fun convert(value: TokenConverterParams): String? { + if (!shouldShowMainPromo) return null + + val currencies = when (value) { + is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies() + is TokenConverterParams.Account -> value.accountList.flattenCurrencies() + }.filter { status -> + status.value is CryptoCurrencyStatus.Loaded || + status.value is CryptoCurrencyStatus.Custom + } + + val tokens = currencies.filter { it.currency is CryptoCurrency.Token } + + if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + + val max = tokens.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val tokenKey = token.yieldSupplyKey() + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.second + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index 1a01d99ec3..d1cbb38064 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -5,16 +5,18 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.utils.coroutines.combine6 +import com.tangem.utils.coroutines.combine7 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import java.math.BigDecimal /** * Subscriber that monitors account list related data and updates the wallet state accordingly. @@ -30,19 +32,21 @@ internal class AccountListSubscriber @AssistedInject constructor( override val clickIntents: WalletClickIntents, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6( + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), flow6 = stakingApyFlow(), + flow7 = yieldSupplyGetShouldShowMainPromoFlow(), transform = ::updateState, ) - private fun yieldSupplyApyFlow(): Flow> { + private fun yieldSupplyApyFlow(): Flow> { return yieldSupplyApyFlowUseCase().distinctUntilChanged() } @@ -50,6 +54,10 @@ internal class AccountListSubscriber @AssistedInject constructor( return stakingApyFlowUseCase().distinctUntilChanged() } + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow { + return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged() + } + @AssistedFactory interface Factory { fun create(userWallet: UserWallet): AccountListSubscriber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 40a9681b13..080766b9b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -20,6 +20,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenCon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import timber.log.Timber +import java.math.BigDecimal /** * Basic implementation of [WalletSubscriber] for wallet with accounts. @@ -46,8 +47,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency: AppCurrency, expandedAccounts: Set, isAccountMode: Boolean, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean = false, ) { val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount @@ -67,6 +69,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { portfolioId = PortfolioId(mainAccount.accountId), yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } isAccountMode -> { @@ -82,7 +85,13 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } else { val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) + updateContent( + params = convertParams, + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, + ) } } } @@ -92,8 +101,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { maybeTokenList: Lce, appCurrency: AppCurrency, portfolioId: PortfolioId, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> @@ -123,14 +133,16 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { stateController.update( SetTokenListTransformer( @@ -140,6 +152,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } 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 e827068c9b..36de78ef22 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 @@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker @@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal @Deprecated("Use AccountListSubscriber instead") @Suppress("LongParameterList") @@ -40,6 +42,7 @@ internal abstract class BasicTokenListSubscriber( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -69,7 +72,8 @@ internal abstract class BasicTokenListSubscriber( flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), flow4 = stakingApyFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap -> + flow5 = yieldSupplyGetShouldShowMainPromoFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -98,6 +102,7 @@ internal abstract class BasicTokenListSubscriber( appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) walletWithFundsChecker.check(tokenList) @@ -122,8 +127,9 @@ internal abstract class BasicTokenListSubscriber( private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map, + yieldSupplyApyMap: Map, stakingApyMap: Map>, + shouldShowMainPromo: Boolean, ) { stateHolder.update( SetTokenListTransformer( @@ -133,6 +139,7 @@ internal abstract class BasicTokenListSubscriber( clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } @@ -146,9 +153,12 @@ internal abstract class BasicTokenListSubscriber( } .distinctUntilChanged() - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() + private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() private fun stakingApyFlow(): Flow>> = stakingApyFlowUseCase() .distinctUntilChanged() + + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() + .distinctUntilChanged() } \ No newline at end of file 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 36220cd6e1..d75d637dad 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 @@ -12,6 +12,7 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -32,6 +33,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -41,6 +43,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e686dd4e20..9e148dcf24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -27,6 +28,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -36,6 +38,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt new file mode 100644 index 0000000000..458cbcaef2 --- /dev/null +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -0,0 +1,208 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.yield.supply.YieldSupplyStatus +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import org.junit.Test +import java.math.BigDecimal + +class YieldSupplyPromoBannerKeyConverterTest { + + @Test + fun `GIVEN promo disabled WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF") + val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) + val tokenList = ungroupedTokenList(status) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = tokenList, + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")), + shouldShowMainPromo = false, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN empty apy map WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") + val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = emptyMap(), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN active yield token present WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") + val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusActive), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.12")), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN multiple candidates EVM case insensitive WHEN convert THEN return key of max amount`() { + val evmNetworkId = "ETH" + val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd") + val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF") + + val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false) + val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) + + val apyMap = mapOf( + "${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), + "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"), + ) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusSmall, statusBig), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + val expectedKey = "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" + assertThat(result).isEqualTo(expectedKey) + } + + @Test + fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() { + val nonEvmId = "xrp" + val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123") + val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) + + val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}" + val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped { + return TokenList.Ungrouped( + totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded( + amount = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), + sortedBy = com.tangem.domain.models.TokensSortType.NONE, + currencies = statuses.toList(), + ) + } + + private fun createStatus( + token: CryptoCurrency.Token, + amount: BigDecimal, + isYieldActive: Boolean, + ): CryptoCurrencyStatus { + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "addr", + type = NetworkAddress.Address.Type.Primary, + ), + ) + val value = CryptoCurrencyStatus.Custom( + amount = amount, + fiatAmount = null, + fiatRate = null, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = if (isYieldActive) { + YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ) + } else { + null + }, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = networkAddress, + sources = CryptoCurrencyStatus.Sources(), + ) + return CryptoCurrencyStatus( + currency = token, + value = value, + ) + } + + private fun createToken(networkId: String, backendId: String, contract: String): CryptoCurrency.Token { + val network = Network( + id = Network.ID(value = networkId, derivationPath = Network.DerivationPath.None), + backendId = backendId, + name = backendId, + currencySymbol = "SYM", + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = when (backendId) { + "ethereum" -> Network.StandardType.ERC20 + else -> Network.StandardType.Unspecified("UNSPEC") + }, + hasFiatFeeRate = false, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(networkId), + suffix = CryptoCurrency.ID.Suffix.ContractAddress(contract), + ), + network = network, + name = "Token", + symbol = "TKN", + decimals = 18, + iconUrl = null, + isCustom = false, + contractAddress = contract, + ) + } +} \ No newline at end of file From f023fa7f2c97f44fd83240837930367a23794bdf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 11:42:49 +0500 Subject: [PATCH 04/33] Updated on 2026-08-14 --- .../supply/models/YieldSupplyRewardBalance.kt | 10 + .../YieldSupplyGetRewardsBalanceUseCase.kt | 98 +++++--- ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 213 ++++++++++++++---- features/tokendetails/impl/build.gradle.kts | 1 + .../tokendetails/TokenDetailsPreviewData.kt | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 5 +- .../state/TokenDetailsBalanceBlockState.kt | 3 +- .../TokenDetailsLoadedBalanceConverter.kt | 4 +- .../state/factory/TokenDetailsStateFactory.kt | 10 +- .../ui/components/TokenDetailsBalanceBlock.kt | 11 +- 10 files changed, 267 insertions(+), 90 deletions(-) create mode 100644 domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt new file mode 100644 index 0000000000..d3086742cb --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyRewardBalance.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.yield.supply.models + +data class YieldSupplyRewardBalance( + val fiatBalance: String?, + val cryptoBalance: String?, +) { + companion object { + fun empty() = YieldSupplyRewardBalance(null, null) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index 52c1434981..93e18928bf 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -1,11 +1,13 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -22,7 +24,7 @@ class YieldSupplyGetRewardsBalanceUseCase( private val dispatcherProvider: CoroutineDispatcherProvider, ) { - operator fun invoke(status: CryptoCurrencyStatus, appCurrency: AppCurrency): Flow = flow { + operator fun invoke(status: CryptoCurrencyStatus, appCurrency: AppCurrency): Flow = flow { val cryptoAmount = status.value.amount val fiatRate = status.value.fiatRate @@ -30,11 +32,7 @@ class YieldSupplyGetRewardsBalanceUseCase( return@flow } - val amount = if (cryptoAmount != null && fiatRate != null) { - cryptoAmount.multiply(fiatRate) - } else { - return@flow - } + if (cryptoAmount == null) return@flow val tokenAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress ?: return@flow val apy = try { @@ -50,37 +48,57 @@ class YieldSupplyGetRewardsBalanceUseCase( return@flow } - val initialPerTickDelta = amount - .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) - .abs() + val initialPerTickDeltaCrypto = perTickDelta(cryptoAmount, apyFraction).abs() - val minVisibleDecimals = calculateMinVisibleDecimals(initialPerTickDelta) + val minVisibleDecimalsCrypto = calculateMinVisibleDecimals( + perTickDeltaAbs = initialPerTickDeltaCrypto, + maxDecimals = status.currency.decimals, + ) - var currentBalance: BigDecimal = amount + val fiatAmountStart = fiatRate?.let { cryptoAmount.multiply(it) } + val minVisibleDecimalsFiat = fiatAmountStart?.let { amount -> + val initialPerTickDeltaFiat = perTickDelta(amount, apyFraction).abs() + calculateMinVisibleDecimals( + perTickDeltaAbs = initialPerTickDeltaFiat, + maxDecimals = FIAT_MAX_DECIMALS, + ) + } + + var currentCryptoBalance: BigDecimal = cryptoAmount + var currentFiatBalance: BigDecimal? = fiatAmountStart while (true) { + val fiatBalanceFormatted: String? = currentFiatBalance?.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).anyDecimals(decimals = minVisibleDecimalsFiat ?: FIAT_MIN_DECIMALS) + } + + val cryptoBalanceFormatted: String = currentCryptoBalance.format { + crypto(status.currency).anyDecimals( + maxDecimals = minVisibleDecimalsCrypto, + minDecimals = minVisibleDecimalsCrypto, + ) + } + emit( - currentBalance.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ).anyDecimals(decimals = minVisibleDecimals) - }, + YieldSupplyRewardBalance(fiatBalance = fiatBalanceFormatted, cryptoBalance = cryptoBalanceFormatted), ) - val perTickDelta = currentBalance - .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + val perTickDeltaCrypto = perTickDelta(currentCryptoBalance, apyFraction) - currentBalance = currentBalance.add(perTickDelta) + currentCryptoBalance = currentCryptoBalance.add(perTickDeltaCrypto) + + currentFiatBalance = currentFiatBalance?.let { current -> + val perTickDeltaFiat = perTickDelta(current, apyFraction) + current.add(perTickDeltaFiat) + } delay(TICK_MILLIS) } }.flowOn(dispatcherProvider.default) - private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal): Int { + private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal, maxDecimals: Int): Int { if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS val perTickAsDouble = perTickDeltaAbs.toDouble() @@ -88,20 +106,28 @@ class YieldSupplyGetRewardsBalanceUseCase( val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble val raw = ceil(-ln(safe) / LN_10) - return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS) + return raw.toInt().coerceIn(MIN_DECIMALS, maxDecimals) } - private companion object { - const val TICK_MILLIS: Long = 800 - private val TICK_SECONDS_BD = BigDecimal("0.8") - private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") // 365 * 24 * 60 * 60 - private val HUNDRED_BD = BigDecimal("100") - private const val SCALE = 18 + private fun perTickDelta(amount: BigDecimal, apyFraction: BigDecimal): BigDecimal { + return amount + .multiply(apyFraction) + .multiply(TICK_SECONDS_BD) + .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + } - private const val MIN_DECIMALS = 3 - private const val MAX_DECIMALS = 12 + companion object { + internal const val TICK_MILLIS: Long = 800 + internal val TICK_SECONDS_BD: BigDecimal = BigDecimal("0.8") + internal val SECONDS_PER_YEAR_BD: BigDecimal = BigDecimal("31536000") // 365 * 24 * 60 * 60 + internal val HUNDRED_BD: BigDecimal = BigDecimal("100") + internal const val SCALE: Int = 18 - private val LN_10 = ln(10.0) - private const val EPSILON = 1e-18 + internal const val MIN_DECIMALS: Int = 3 + internal const val FIAT_MIN_DECIMALS: Int = 2 + internal const val FIAT_MAX_DECIMALS: Int = 12 + + internal val LN_10: Double = ln(10.0) + internal const val EPSILON: Double = 1e-18 } } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index 3bdd2f446e..c086813f73 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -11,6 +12,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase.Companion.TICK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.mockk @@ -26,7 +28,6 @@ import org.junit.jupiter.api.Test import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.ceil -import kotlin.math.ln class YieldSupplyGetRewardsBalanceUseCaseTest { @@ -165,9 +166,9 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { val deferred = async { useCase(status, appCurrency).take(3).toList() } testScheduler.advanceUntilIdle() - advanceTimeBy(300) + advanceTimeBy(TICK_MILLIS) testScheduler.advanceUntilIdle() - advanceTimeBy(300) + advanceTimeBy(TICK_MILLIS) testScheduler.advanceUntilIdle() val collected = deferred.await() @@ -175,25 +176,147 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { val expectedDecimals = calculateMinVisibleDecimalsForTest(initialPerTickDelta(amount, apy)) - val firstExpected = amount.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[0]).isEqualTo(firstExpected) + val firstExpected = amount.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[0].fiatBalance).isEqualTo(firstExpected) val firstNext = nextBalance(amount, apy) - val secondExpected = firstNext.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[1]).isEqualTo(secondExpected) + val secondExpected = firstNext.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[1].fiatBalance).isEqualTo(secondExpected) val secondNext = nextBalance(firstNext, apy) - val thirdExpected = secondNext.format { fiat( - appCurrency.code, - appCurrency.symbol, - ).anyDecimals(decimals = expectedDecimals) } - assertThat(collected[2]).isEqualTo(thirdExpected) + val thirdExpected = secondNext.format { + fiat( + appCurrency.code, + appCurrency.symbol, + ).anyDecimals(decimals = expectedDecimals) + } + assertThat(collected[2].fiatBalance).isEqualTo(thirdExpected) + } + + @Test + fun `GIVEN polygon USDT0 Loaded status WHEN invoke THEN emit formatted balances`() = runTest { + val network = Network( + id = Network.ID(Network.RawID("POLYGON"), Network.DerivationPath.Card("m/44'/60'/0'/0/0")), + backendId = "polygon-pos", + name = "Polygon", + currencySymbol = "POL", + derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0"), + isTestnet = false, + standardType = Network.StandardType.Unspecified("Polygon"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + val tokenId = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(network.rawId), + suffix = CryptoCurrency.ID.Suffix.RawID("usdt0", "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"), + ) + val currency = CryptoCurrency.Token( + id = tokenId, + network = network, + name = "USDT0", + symbol = "USDT0", + decimals = 6, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/usdt0.png", + isCustom = false, + contractAddress = "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + ) + + val amount = BigDecimal("9.241136") + val fiatRate = BigDecimal("0.9999761277273864") + val status = CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = amount.multiply(fiatRate), + fiatRate = fiatRate, + priceChange = BigDecimal("-0.000058200000000008245"), + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address( + value = "0xb71fa0E20ba8579B3ec51cC79aaa84Bf5982BB49", + type = NetworkAddress.Address.Type.Primary, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + val apy = BigDecimal("5.0") + coEvery { repository.getCachedMarkets() } returns listOf( + YieldMarketToken( + tokenAddress = currency.contractAddress, + chainId = 137, + apy = apy, + isActive = true, + maxFeeNative = BigDecimal.ZERO, + maxFeeUSD = BigDecimal.ZERO, + backendId = "polygon-pos", + ), + ) + + val dispatcherProvider = testDispatcherProvider(this) + val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider) + val appCurrency = AppCurrency.Default + + val deferred = async { useCase(status, appCurrency).take(2).toList() } + + testScheduler.advanceUntilIdle() + advanceTimeBy(TICK_MILLIS) + testScheduler.advanceUntilIdle() + + val emissions = deferred.await() + assertThat(emissions).hasSize(2) + + val apyFraction = apy.divide(BigDecimal("100"), 18, RoundingMode.HALF_UP) + val perTickCrypto = amount.multiply(apyFraction) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) + .abs() + val minCryptoDecimals = calculateMinVisibleDecimalsForTest(perTickCrypto).coerceAtMost(currency.decimals) + + val fiatAmountStart = amount.multiply(fiatRate) + val perTickFiat = fiatAmountStart.multiply(apyFraction) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) + .abs() + val minFiatDecimals = calculateMinVisibleDecimalsForTest(perTickFiat) + + val expectedCrypto0 = amount.format { + crypto(currency).anyDecimals( + maxDecimals = minCryptoDecimals, + minDecimals = minCryptoDecimals, + ) + } + val expectedFiat0 = fiatAmountStart.format { + fiat(appCurrency.code, appCurrency.symbol).anyDecimals(decimals = minFiatDecimals) + } + + assertThat(emissions[0].cryptoBalance).isEqualTo(expectedCrypto0) + assertThat(emissions[0].fiatBalance).isEqualTo(expectedFiat0) } private fun testDispatcherProvider(scope: TestScope): CoroutineDispatcherProvider { @@ -260,42 +383,48 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { } private fun initialPerTickDelta(amount: BigDecimal, apy: BigDecimal): BigDecimal { - val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP) + val apyFraction = apy.divide( + YieldSupplyGetRewardsBalanceUseCase.HUNDRED_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) return amount .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) .abs() } private fun nextBalance(current: BigDecimal, apy: BigDecimal): BigDecimal { - val apyFraction = apy.divide(HUNDRED_BD, SCALE, RoundingMode.HALF_UP) + val apyFraction = apy.divide( + YieldSupplyGetRewardsBalanceUseCase.HUNDRED_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) val perTickDelta = current .multiply(apyFraction) - .multiply(TICK_SECONDS_BD) - .divide(SECONDS_PER_YEAR_BD, SCALE, RoundingMode.HALF_UP) + .multiply(YieldSupplyGetRewardsBalanceUseCase.TICK_SECONDS_BD) + .divide( + YieldSupplyGetRewardsBalanceUseCase.SECONDS_PER_YEAR_BD, + YieldSupplyGetRewardsBalanceUseCase.SCALE, + RoundingMode.HALF_UP, + ) return current.add(perTickDelta) } private fun calculateMinVisibleDecimalsForTest(perTickDeltaAbs: BigDecimal): Int { - if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS + if (perTickDeltaAbs <= BigDecimal.ZERO) return YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS val perTickAsDouble = perTickDeltaAbs.toDouble() - if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return MIN_DECIMALS - val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble - val raw = ceil(-ln(safe) / LN_10) - return raw.toInt().coerceIn(MIN_DECIMALS, MAX_DECIMALS) - } - - private companion object { - private const val SCALE = 18 - private val TICK_SECONDS_BD = BigDecimal("0.8") - private val SECONDS_PER_YEAR_BD = BigDecimal("31536000") - private val HUNDRED_BD = BigDecimal("100") - - private const val MIN_DECIMALS = 3 - private const val MAX_DECIMALS = 12 - - private val LN_10 = ln(10.0) - private const val EPSILON = 1e-18 + if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS + val safe = if (perTickAsDouble <= 0.0) YieldSupplyGetRewardsBalanceUseCase.EPSILON else perTickAsDouble + val raw = ceil(-kotlin.math.ln(safe) / YieldSupplyGetRewardsBalanceUseCase.LN_10) + return raw.toInt().coerceIn( + YieldSupplyGetRewardsBalanceUseCase.MIN_DECIMALS, + YieldSupplyGetRewardsBalanceUseCase.FIAT_MAX_DECIMALS, + ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 2d9665f967..05ec40ad40 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.yieldSupply) + implementation(projects.domain.yieldSupply.models) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 4e70a2faa4..a221f254ec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -121,7 +121,7 @@ internal object TokenDetailsPreviewData { selectedBalanceType = BalanceType.ALL, onBalanceSelect = {}, displayCryptoBalance = "966,96 XLM", - displayYeildSupplyCryptoBalance = null, + displayYieldSupplyFiatBalance = null, displayFiatBalance = "91,50$", isBalanceSelectorEnabled = true, isBalanceFlickering = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 3ef6e97629..16ce0aecda 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -75,6 +75,7 @@ import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter @@ -416,7 +417,9 @@ internal class TokenDetailsModel @Inject constructor( .saveIn(yieldSupplyBalanceJobHolder) } else { yieldSupplyBalanceJobHolder.cancel() - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(null) + internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( + YieldSupplyRewardBalance.empty(), + ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index a74d80f6b8..9e17b95c95 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -28,7 +28,8 @@ internal sealed class TokenDetailsBalanceBlockState { val isBalanceSelectorEnabled: Boolean, val isBalanceFlickering: Boolean, val yieldSupplyState: TokenDetailsYieldSupplyState = TokenDetailsYieldSupplyState.Empty, - val displayYeildSupplyCryptoBalance: String? = null, + val displayYieldSupplyFiatBalance: String? = null, + val displayYieldSupplyCryptoBalance: String? = null, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 3243636297..306315e766 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -98,8 +98,8 @@ internal class TokenDetailsLoadedBalanceConverter( stakingCryptoAmount, currentState.selectedBalanceType, ), - displayYeildSupplyCryptoBalance = (currentState as? TokenDetailsBalanceBlockState.Content) - ?.displayYeildSupplyCryptoBalance, + displayYieldSupplyFiatBalance = (currentState as? TokenDetailsBalanceBlockState.Content) + ?.displayYieldSupplyFiatBalance, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e19dda0395..da13ce39ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -26,6 +26,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig @@ -315,13 +316,18 @@ internal class TokenDetailsStateFactory( return balanceSelectStateConverter.convert(buttonConfig) } - fun getStateWithUpdatedYieldSupplyDisplayBalance(displayBalance: String?): TokenDetailsState { + fun getStateWithUpdatedYieldSupplyDisplayBalance( + yieldSupplyRewardBalance: YieldSupplyRewardBalance, + ): TokenDetailsState { val state = currentStateProvider() val balanceState = state.tokenBalanceBlockState return state.copy( tokenBalanceBlockState = when (balanceState) { is TokenDetailsBalanceBlockState.Content -> - balanceState.copy(displayYeildSupplyCryptoBalance = displayBalance) + balanceState.copy( + displayYieldSupplyFiatBalance = yieldSupplyRewardBalance.fiatBalance, + displayYieldSupplyCryptoBalance = yieldSupplyRewardBalance.cryptoBalance, + ) is TokenDetailsBalanceBlockState.Error -> balanceState is TokenDetailsBalanceBlockState.Loading -> balanceState }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 9eaa517709..293e54545c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -122,12 +122,12 @@ private fun FiatBalance( height = TangemTheme.dimens.size32, ), ) - is TokenDetailsBalanceBlockState.Content -> if (state.displayYeildSupplyCryptoBalance != null && + is TokenDetailsBalanceBlockState.Content -> if (state.displayYieldSupplyFiatBalance != null && !isBalanceHidden ) { TextAnimatedCounter( modifier = modifier, - text = state.displayYeildSupplyCryptoBalance, + text = state.displayYieldSupplyFiatBalance, style = TangemTheme.typography.h2.applyBladeBrush( isEnabled = state.isBalanceFlickering, textColor = TangemTheme.colors.text.primary1, @@ -136,7 +136,7 @@ private fun FiatBalance( } else { Text( modifier = modifier, - text = (state.displayYeildSupplyCryptoBalance ?: state.displayFiatBalance).orMaskWithStars( + text = (state.displayYieldSupplyFiatBalance ?: state.displayFiatBalance).orMaskWithStars( isBalanceHidden, ), style = TangemTheme.typography.h2.applyBladeBrush( @@ -184,8 +184,9 @@ private fun CryptoBalance( tint = TangemTheme.colors.icon.inactive, contentDescription = null, ) - Text( - text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden), + TextAnimatedCounter( + text = (state.displayYieldSupplyCryptoBalance ?: state.displayCryptoBalance) + .orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2.applyBladeBrush( isEnabled = state.isBalanceFlickering, textColor = TangemTheme.colors.text.tertiary, From aa9956d69f613b824febfbc22682f4908938927e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 11:44:10 +0500 Subject: [PATCH 05/33] Updated on 2026-08-14 --- .../features/yield/supply/impl/main/model/YieldSupplyModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index c8f25dea1a..2c16d93d51 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -91,7 +91,7 @@ internal class YieldSupplyModel @Inject constructor( subscribeOnCurrencyStatusUpdates() singleNetworkStatusFetcher( params = SingleNetworkStatusFetcher.Params( - userWalletId = userWallet.walletId, + userWalletId = params.userWalletId, network = cryptoCurrency.network, ), ) From 8cee77705fa9ce2cad080047b399c5e13a1a49ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 13:16:10 +0500 Subject: [PATCH 06/33] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 6 ++++ .../currency/CryptoCurrencyExtensions.kt | 4 +-- .../YieldSupplyGetDustMinAmountUseCase.kt | 20 ++++++++++++ .../YieldSupplyGetDustMinAmountUseCaseTest.kt | 31 +++++++++++++++++++ .../active/model/YieldSupplyActiveModel.kt | 3 ++ .../YieldSupplyActiveMinAmountTransformer.kt | 6 ++-- .../impl/main/model/YieldSupplyModel.kt | 16 ++++++++-- 7 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt create mode 100644 domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index 28f00e7e98..b8c7e6dfc7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -219,4 +219,10 @@ internal object YieldSupplyDomainModule { yieldSupplyRepository = yieldSupplyRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase { + return YieldSupplyGetDustMinAmountUseCase() + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index 4937c3c0af..09c43362de 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -11,9 +11,9 @@ fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean { return notSupplied > BigDecimal.ZERO } -fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount: BigDecimal): Boolean { +fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount: BigDecimal): Boolean { val notSupplied = notSuppliedAmountOrNull() ?: return false - return notSupplied >= minAmount + return notSupplied >= dustAmount } fun CryptoCurrencyStatus.notSuppliedAmountOrNull(): BigDecimal? { diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt new file mode 100644 index 0000000000..7814d60add --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +class YieldSupplyGetDustMinAmountUseCase { + + operator fun invoke(minAmount: BigDecimal, appCurrency: AppCurrency): BigDecimal { + return if (appCurrency.code in SUPPORTED_DUST_CURRENCIES) { + DUST_MIN_AMOUNT + } else { + minAmount.stripTrailingZeros() + } + } + + companion object { + private val DUST_MIN_AMOUNT = BigDecimal("0.1") + private val SUPPORTED_DUST_CURRENCIES = setOf("EUR", "USD", "AUD", "CAD", "GBP") + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt new file mode 100644 index 0000000000..50a00ac69c --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.yield.supply.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.appcurrency.model.AppCurrency +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class YieldSupplyGetDustMinAmountUseCaseTest { + + private val useCase = YieldSupplyGetDustMinAmountUseCase() + + @Test + fun `GIVEN supported currency WHEN invoke THEN return dust min amount`() { + val minAmount = BigDecimal("123.456") + val appCurrency = AppCurrency(code = "EUR", name = "Euro", symbol = "€") + + val result = useCase(minAmount, appCurrency) + + assertThat(result).isEqualTo(BigDecimal("0.1")) + } + + @Test + fun `GIVEN unsupported currency WHEN invoke THEN return min amount stripped`() { + val minAmount = BigDecimal("1.2300") + val appCurrency = AppCurrency(code = "JPY", name = "Japanese Yen", symbol = "¥") + + val result = useCase(minAmount, appCurrency) + + assertThat(result).isEqualTo(BigDecimal("1.23")) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index 8dbc13d8c2..d468e6d60d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -58,6 +58,7 @@ internal class YieldSupplyActiveModel @Inject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val urlOpener: UrlOpener, private val appRouter: AppRouter, + private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, ) : Model(), YieldSupplyStopEarningComponent.ModelCallback, YieldSupplyApproveComponent.ModelCallback { @@ -241,11 +242,13 @@ internal class YieldSupplyActiveModel @Inject constructor( userWalletId, cryptoCurrencyStatusFlow.value, ).onRight { minAmount -> + val dustAmount = yieldSupplyGetDustMinAmountUseCase(minAmount = minAmount, appCurrency = appCurrency) uiState.update( YieldSupplyActiveMinAmountTransformer( cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value, appCurrency = appCurrency, minAmount = minAmount, + dustMinAmount = dustAmount, analyticsHandler = analyticsHandler, onApprove = ::onApprove, ), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt index c3b1fd4763..54dd971067 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt @@ -28,11 +28,12 @@ import java.math.BigDecimal * - Builds the fee policy note text using the minimum amount. * - Adds contextual notifications: * - Approval required notification when spending is not yet allowed (emits analytics on CTA). - * - "Not all amount supplied" info when wallet balance exceeds the supplied balance by more than [minAmount]. + * - "Not all amount supplied" info when the not-supplied balance exceeds the dust threshold [dustMinAmount]. * * @property cryptoCurrencyStatus Current currency status used to calculate values and flags. * @property appCurrency Preferred fiat currency for formatting. * @property minAmount Protocol-required minimal amount to deposit/supply (in crypto units). + * @property dustMinAmount Threshold used to detect dust/not-supplied balance (in crypto units). * @property analyticsHandler Analytics reporter for user actions. * @property onApprove Action invoked when the "Approve" notification button is tapped. */ @@ -40,6 +41,7 @@ internal class YieldSupplyActiveMinAmountTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, private val minAmount: BigDecimal, + private val dustMinAmount: BigDecimal, private val analyticsHandler: AnalyticsEventHandler, private val onApprove: () -> Unit, ) : Transformer { @@ -89,7 +91,7 @@ internal class YieldSupplyActiveMinAmountTransformer( } private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? { - return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)) { + return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustMinAmount)) { val cryptoCurrency = cryptoCurrencyStatus.currency val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull() val formattedAmount = diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 2c16d93d51..266b8f2df4 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.yield.supply.impl.main.model +import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import android.os.SystemClock import com.tangem.common.routing.AppRoute.YieldSupplyPromo @@ -12,6 +13,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,6 +31,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetDustMinAmountUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics @@ -55,6 +59,7 @@ internal class YieldSupplyModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventsHandler: AnalyticsEventHandler, private val appRouter: AppRouter, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, @@ -65,6 +70,7 @@ internal class YieldSupplyModel @Inject constructor( private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, + private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() @@ -73,6 +79,7 @@ internal class YieldSupplyModel @Inject constructor( field = MutableStateFlow(YieldSupplyUM.Initial) private val cryptoCurrency = params.cryptoCurrency + private var appCurrency: AppCurrency = AppCurrency.Default var userWallet: UserWallet by Delegates.notNull() private val fetchCurrencyJobHolder = JobHolder() @@ -85,7 +92,8 @@ internal class YieldSupplyModel @Inject constructor( } private fun checkIfYieldSupplyIsAvailable() { - modelScope.launch(dispatchers.io) { + modelScope.launch(dispatchers.default) { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency) if (isAvailable) { subscribeOnCurrencyStatusUpdates() @@ -345,7 +353,11 @@ internal class YieldSupplyModel @Inject constructor( val minAmount = yieldSupplyMinAmountUseCase(userWalletId = userWallet.walletId, cryptoCurrencyStatus) .getOrNull() if (minAmount != null) { - cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount) + val dustAmount = yieldSupplyGetDustMinAmountUseCase( + minAmount = minAmount, + appCurrency = appCurrency, + ) + cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount) } else { false } From afa13d836e7aca0cca44dfb10a375449b45a07c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 19:07:37 +0700 Subject: [PATCH 07/33] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 6 ----- .../network/ethereum/WcEthNetwork.kt | 4 +--- .../network/solana/WcSolanaNetwork.kt | 4 +--- .../pair/CaipNamespaceDelegate.kt | 4 +--- .../utils/WcNetworksConverter.kt | 22 +++++++++++++++---- .../entity/common/WcCommonTransactionModel.kt | 4 ++++ .../transaction/model/WcAddNetworkModel.kt | 2 +- 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index b0e9c01c6e..9b04683948 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -148,14 +148,12 @@ internal object WalletConnectDataModule { @SdkMoshi moshi: Moshi, sessionsManager: WcSessionsManager, factories: WcEthNetwork.Factories, - walletManagersFacade: WalletManagersFacade, wcNetworksConverter: WcNetworksConverter, ): WcEthNetwork = WcEthNetwork( moshi = moshi, networksConverter = wcNetworksConverter, sessionsManager = sessionsManager, factories = factories, - walletManagersFacade = walletManagersFacade, ) @Provides @@ -165,24 +163,20 @@ internal object WalletConnectDataModule { wcNetworksConverter: WcNetworksConverter, sessionsManager: WcSessionsManager, factories: WcSolanaNetwork.Factories, - walletManagersFacade: WalletManagersFacade, ): WcSolanaNetwork = WcSolanaNetwork( moshi = moshi, sessionsManager = sessionsManager, factories = factories, networksConverter = wcNetworksConverter, - walletManagersFacade = walletManagersFacade, ) @Provides @Singleton fun caipNamespaceDelegate( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, - walletManagersFacade: WalletManagersFacade, wcNetworksConverter: WcNetworksConverter, ): CaipNamespaceDelegate = CaipNamespaceDelegate( namespaceConverters = namespaceConverters, - walletManagersFacade = walletManagersFacade, wcNetworksConverter = wcNetworksConverter, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index a14460a566..b713e9f18b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -18,7 +18,6 @@ import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import jakarta.inject.Inject internal class WcEthNetwork( @@ -26,7 +25,6 @@ internal class WcEthNetwork( private val sessionsManager: WcSessionsManager, private val factories: Factories, private val networksConverter: WcNetworksConverter, - private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? { @@ -57,7 +55,7 @@ internal class WcEthNetwork( is WcEthMethod.SwitchEthereumChain, -> anyExistNetwork() - ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() } val walletNetwork = when (method) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index ebaf668ba0..db6f83849d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -22,7 +22,6 @@ import com.tangem.domain.walletconnect.model.WcSolanaMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import jakarta.inject.Inject internal class WcSolanaNetwork( @@ -30,7 +29,6 @@ internal class WcSolanaNetwork( private val sessionsManager: WcSessionsManager, private val factories: Factories, private val networksConverter: WcNetworksConverter, - private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? { @@ -52,7 +50,7 @@ internal class WcSolanaNetwork( val chainId = request.chainId.orEmpty() suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) suspend fun anyAddress() = anyExistNetwork() - ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() val accountAddress = when (method) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt index 4b5d2c336a..3013709c9f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -9,11 +9,9 @@ import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.model.WcSessionApprove -import com.tangem.domain.walletmanager.WalletManagersFacade internal class CaipNamespaceDelegate( private val namespaceConverters: Set, - private val walletManagersFacade: WalletManagersFacade, private val wcNetworksConverter: WcNetworksConverter, ) { @@ -36,7 +34,7 @@ internal class CaipNamespaceDelegate( } suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? { - val address = walletManagersFacade.getDefaultAddress(userWalletId, network) + val address = wcNetworksConverter.getAddressForWC(userWalletId, network) val chainId = allWcNetworks .find { (wcNetwork, _) -> network.rawId == wcNetwork.rawId } ?.second diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 880b142dbd..a494eb772e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -1,6 +1,9 @@ package com.tangem.data.walletconnect.utils import com.reown.walletkit.client.Wallet +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.domain.account.producer.SingleAccountProducer @@ -44,7 +47,7 @@ internal class WcNetworksConverter @Inject constructor( val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet) val requestNetwork = allCoinNetwork.find { network -> - val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network) + val address = getAddressForWC(wallet.walletId, network) requestAddress.lowercase() == address?.lowercase() } return requestNetwork @@ -60,7 +63,18 @@ internal class WcNetworksConverter @Inject constructor( suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List { return filterWalletNetworkForRequest(rawChainId, wallet) - .mapNotNull { walletManagersFacade.getDefaultAddress(wallet.walletId, it)?.lowercase() } + .mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() } + } + + suspend fun getAddressForWC(userWalletId: UserWalletId, network: Network): String? { + return when (network.toBlockchain()) { + Blockchain.XDC, + Blockchain.XDCTestnet, + -> walletManagersFacade.getAddresses(userWalletId, network) + .find { address -> address.type == AddressType.Legacy } + ?.value + else -> walletManagersFacade.getDefaultAddress(userWalletId, network) + } } /** @@ -94,8 +108,8 @@ internal class WcNetworksConverter @Inject constructor( // find all derivation .filter { it.rawId == blockchain.id } // find equal address - .firstOrNull { - val walletAddress = walletManagersFacade.getDefaultAddress(wallet.walletId, it) + .firstOrNull { network -> + val walletAddress = getAddressForWC(wallet.walletId, network) walletAddress?.lowercase() == caip10.accountAddress.lowercase() } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt index 5acb41c803..038b9106de 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt @@ -20,4 +20,8 @@ internal interface WcCommonTransactionModel { fun showSuccessSignMessage(message: TextReference = resourceReference(R.string.wc_successfully_signed)) { messageSender.send(ToastMessage(message = message)) } + + fun showSuccessAddedMessage(message: TextReference = resourceReference(R.string.common_added)) { + messageSender.send(ToastMessage(message = message)) + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 08176516ce..8ed61a1b5d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -111,7 +111,7 @@ internal class WcAddNetworkModel @Inject constructor( modelScope.launch { _uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = true)) } useCase.approve().getOrNull()?.let { - showSuccessSignMessage() + showSuccessAddedMessage() router.pop() } ?: run { _uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = false)) } From 50dca505084263f42ea2801ddb708018fece6abb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 13:26:18 +0400 Subject: [PATCH 08/33] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 6 + core/res/src/main/res/values-de/strings.xml | 3 + core/res/src/main/res/values-ja/strings.xml | 6 +- core/res/src/main/res/values-ru/strings.xml | 26 ++ .../yield/supply/models/YieldSupplyFee.kt | 8 + .../YieldSupplyGetCurrentFeeUseCase.kt | 24 +- .../YieldSupplyGetCurrentFeeUseCaseTest.kt | 384 ++++++++++++++++++ .../active/model/YieldSupplyActiveModel.kt | 2 +- .../YieldSupplyNotificationsComponent.kt | 1 - .../entity/YieldSupplyNotificationData.kt | 1 + .../model/YieldSupplyNotificationsModel.kt | 4 + .../model/YieldSupplyStartEarningModel.kt | 3 +- 12 files changed, 460 insertions(+), 8 deletions(-) create mode 100644 domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt create mode 100644 domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 7425a7aa0f..fe9115cfec 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -301,6 +301,12 @@ sealed class NotificationUM(val config: NotificationConfig) { ), subtitle = TextReference.EMPTY, ) + + data object YieldSupplyHighNetworkFee : Info( + title = resourceReference(id = R.string.yield_module_high_network_fees_notification_title), + subtitle = resourceReference(id = R.string.yield_module_high_network_fees_notification_description), + iconTint = NotificationConfig.IconTint.Accent, + ) } sealed interface Cardano { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index a009fa485f..0dc6af1380 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -849,6 +849,9 @@ Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset. Sicher Dir 10 $ in BTC mit jeder Wallet greif schnell zu! Black Friday: bis zu 30% Nachlass + Los geht\'s + Nur für kurze Zeit! + 1+1: Beim Kauf einer Wallet erhältst Du 50% Rabatt Jetzt beitreten Teile Deinen Code – verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt. Erhalte BELOHNUNGEN für jeden Freund! diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 94e4382158..82aca73289 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -848,8 +848,8 @@ ウォレットごとに$10相当のBTCをプレゼント\nお早めに! ブラックフライデー:最大30% オフ 今すぐチェック - 理想のTangemセットを揃えよう。期間限定。 - 1+1:1つ購入で、2つ目が50%オフ + 期間限定! + 1+1:ウォレット1つ購入で、2つ目が50%オフ 今すぐ参加 コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 友達への紹介で報酬を獲得しよう! @@ -1391,7 +1391,7 @@ 取引を表示 サービス手数料 手数料 - カードを紛失したり盗まれたりしても、資金を安全に保てます。一時停止はいつでも解除できます。 + 資金を安全に保管します。凍結はいつでも解除できます。 カードを一時停止しますか? カードを凍結できませんでした。しばらくしてからもう一度お試しください。 一時停止 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b5587d3945..4b22469076 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -16,8 +16,10 @@ Код доступа Превышен лимит %1$s аккаунтов. Архивируйте, чтобы создать новый. Невозможно добавить новый аккаунт. + Аккаунт заархивирован Архивированные аккаунты Восстановить + Вы собираетесь восстановить \"%1$s\". Восстановить аккаунт Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить. Невозможно восстановить аккаунт @@ -26,12 +28,14 @@ Этот аккаунт участвует в реферальной программе Этот аккаунт не может быть архивирован. Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже. + Аккаунт создан Архивный аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... Аккаунт Невозможно отредактировать аккаунт + Аккаунт сохранен Аккаунт для наград Добавить аккаунт Сохранить @@ -46,6 +50,8 @@ Основной аккаунт Лимит %1$s активных аккаунтов превышен. Архивируйте один, чтобы продолжить. Невозможно восстановить аккаунт + Аккаунт восстановлен + Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов. Продолжить Отменить Вы уверены, что хотите создание нового аккаунта? @@ -87,14 +93,17 @@ Токены в сети %1$s не поддерживаются этой картой или кольцом из-за ограничений прошивки. У вас возникли трудности со сканированием карты или кольца? Эта карта не предназначена для работы с этим приложением + Сначала установите код доступа, чтобы включить биометрию. Используйте %1$s, чтобы быстро и безопасно разблокировать кошелёк и выполнять чувствительные действия, например, подписывать транзакции. Для аппаратных кошельков всё ещё требуется карта или кольцо для подписи. Комиссия по-умолчанию Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem Включите биометрическую аутентификацию Отключение %1$s потребует ввода вашего кода доступа для разблокировки приложения и работы с кошельком. + Позже будет необходимо ввести код доступа к вашему кошельку, чтобы мы могли безопасно сохранить его для будущего использования. Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. + Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа. Требуется код доступа Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа. Сохранение кода доступа @@ -144,6 +153,7 @@ Вход по биометрии временно заблокирован. Попробуйте снова через 30 секунд или разблокируйте кошелёк с помощью прикладывания устройства или кода доступа. Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Биометрические данные на вашем устройстве были обновлены. Пожалуйста, выберите свой кошелёк и введите его код доступа, чтобы повторно включить вход по биометрии. Внимание При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. Ошибка активации @@ -299,6 +309,7 @@ Позже Узнать больше Осталось %1$s + Легаси Bitcoin Заблокирован Заблокированные кошельки Основная сеть @@ -311,6 +322,8 @@ %d сетей %d сети + Новый адрес + Новости Далее NFT Нет @@ -363,6 +376,7 @@ Поддержка Поддерживаемые сети Обменять + Tangem условия участия Условиями использования На @@ -391,6 +405,7 @@ Адрес контракта скопирован! Доступные сети Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи. + Деривация принадлежит другому аккаунту Токен добавлен в аккаунт %1$s Адрес контракта Адрес контракта некорректен @@ -527,6 +542,8 @@ ID: %s ID транзакции скопирован Обменяйте любой актив в вашем портфеле на этот токен + Рынок и Новости + В тренде Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта или кольцо? @@ -687,6 +704,7 @@ APY %s Мой портфель Рынок + Зарабатывай с Tangem Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька Потяните вверх или коснитесь поисковой строки, чтобы добавить токен Данные раздела получены из следующих сетей: %s @@ -791,6 +809,8 @@ Активировать режим доходности Обновитесь до версии %1$s, чтобы создать мобильный кошелёк Мобильный кошелек требует %1$s или новее. + Все новости + Будьте в курсе Функция NFC недоступна на вашем устройстве О NFT NFT @@ -1198,6 +1218,7 @@ %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Сумма превышает остаток Для завершения транзакции на указанный адрес необходим тег назначения (мемо). + Требуется Destination Tag Недопустимая сумма Комиссия превышает остаток Отправляемая сумма превышает остаток @@ -1225,6 +1246,8 @@ Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне. Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге. Активация аккаунта + Сетевая комиссия изменилась. Пожалуйста, проверьте новую сумму перед продолжением. + Комиссия сети обновлена Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. @@ -1263,6 +1286,7 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга. Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s Заблокировано + Максимальная сумму: %s Переместить Нативный стейкинг В данный момент нет доступных активных валидаторов для стейкинга. Пожалуйста, попробуйте позже. @@ -1598,6 +1622,7 @@ Код доступа Активация аккаунта ПИН не принят. Попробуйте ещё раз или введите другой код. + Разблокировать Выберите способ добавления кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. Создать аппаратный кошелёк @@ -1934,6 +1959,7 @@ Политика комиссий за пополнение Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода. Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. + Высокая сетевая комиссия Историческая доходность Получай до %1$s APY на свой баланс Разрешение для вашего токена в режиме доходности было отозвано. Откройте токен, чтобы выдать разрешение снова. diff --git a/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt new file mode 100644 index 0000000000..6763e16cf5 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyFee.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.yield.supply.models + +import java.math.BigDecimal + +data class YieldSupplyFee( + val value: BigDecimal, + val isHighFee: Boolean = false, +) \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt index 460440f4b4..30daafec70 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt @@ -2,6 +2,8 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import arrow.core.Either.Companion.catch +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWalletId @@ -10,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.yield.supply.YieldSupplyConst.YIELD_SUPPLY_EVM_CONSTANT_GAS_LIMIT import com.tangem.domain.yield.supply.fixFee +import com.tangem.domain.yield.supply.models.YieldSupplyFee import java.math.BigDecimal import java.math.RoundingMode @@ -25,7 +28,7 @@ class YieldSupplyGetCurrentFeeUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Either = catch { + ): Either = catch { val feeWithoutGas = feeRepository.getEthereumFeeWithoutGas(userWalletId, cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate ?: error("Fiat rate is missing") @@ -60,6 +63,23 @@ class YieldSupplyGetCurrentFeeUseCase( val tokenValue = rateRatio.multiply(nativeGas.amount.value) - tokenValue.stripTrailingZeros() + val isEthereum = cryptoCurrencyStatus.currency + .network.id.rawId.value == Blockchain.Ethereum.id + + val isHighFee = if (isEthereum) { + val maxFeePerGas = (feeWithoutGas as? Fee.Ethereum.EIP1559)?.maxFeePerGas ?: 0.toBigInteger() + maxFeePerGas >= HIGH_ETHEREUM_FEE + } else { + false + } + + YieldSupplyFee( + value = tokenValue.stripTrailingZeros(), + isHighFee = isHighFee, + ) + } + + companion object { + private val HIGH_ETHEREUM_FEE = 400_000_000.toBigInteger() } } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt new file mode 100644 index 0000000000..ee27141e2d --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt @@ -0,0 +1,384 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.utils.convertToSdkAmount +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode + +@OptIn(ExperimentalCoroutinesApi::class) +class YieldSupplyGetCurrentFeeUseCaseTest { + + private val feeRepository: FeeRepository = mockk() + private val quotesRepository: QuotesRepository = mockk() + private val currenciesRepository: CurrenciesRepository = mockk() + + private lateinit var useCase: YieldSupplyGetCurrentFeeUseCase + + private val userWalletId = UserWalletId("abcdef012345") + + @BeforeEach + fun setUp() { + useCase = YieldSupplyGetCurrentFeeUseCase( + feeRepository = feeRepository, + quotesRepository = quotesRepository, + currenciesRepository = currenciesRepository, + ) + } + + @Test + fun `GIVEN valid inputs on non-ethereum WHEN invoke THEN returns fee value and not high`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val tokenDecimals = 8 + val nativeDecimals = 18 + val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals) + val nativeFiatRate = BigDecimal("4.00") + + val maxFeePerGas = BigInteger.valueOf(1_000_000_000L) + val feeWithoutGas = Fee.Ethereum.EIP1559( + maxFeePerGas = maxFeePerGas, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = nativeFiatRate, + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val gasLimit = BigInteger.valueOf(350_000) + val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals) + val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP) + val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isRight()).isTrue() + val fee = (result as Either.Right).value + assertThat(fee.value).isEqualTo(expectedTokenValue) + assertThat(fee.isHighFee).isFalse() + } + + @Test + fun `GIVEN ethereum with high gas WHEN invoke THEN returns high fee flag`() = runTest { + val rawNetworkId = Blockchain.Ethereum.id + val tokenDecimals = 8 + val nativeDecimals = 18 + val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = nativeDecimals) + val nativeFiatRate = BigDecimal("4.00") + + val maxFeePerGas = BigInteger.valueOf(400_000_000L) // threshold + val feeWithoutGas = Fee.Ethereum.EIP1559( + maxFeePerGas = maxFeePerGas, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns feeWithoutGas + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = nativeFiatRate, + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val gasLimit = BigInteger.valueOf(350_000) + val nativeGasValue = maxFeePerGas.multiply(gasLimit).toBigDecimal().movePointLeft(nativeDecimals) + val rateRatio = nativeFiatRate.divide(cryptoStatus.value.fiatRate!!, tokenDecimals, RoundingMode.HALF_UP) + val expectedTokenValue = rateRatio.multiply(nativeGasValue).stripTrailingZeros() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isRight()).isTrue() + val fee = (result as Either.Right).value + assertThat(fee.value).isEqualTo(expectedTokenValue) + assertThat(fee.isHighFee).isTrue() + } + + @Test + fun `GIVEN token fiat rate missing WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = null) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.ONE, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount( + createStatus(token = token, fiatRate = BigDecimal.ONE), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Fiat rate is missing") + } + + @Test + fun `GIVEN native quotes unavailable WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns null + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Quotes for native coin are unavailable") + } + + @Test + fun `GIVEN empty quotes list WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) } returns emptySet() + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalStateException::class.java) + assertThat(error.message).isEqualTo("Empty quotes list for native coin") + } + + @Test + fun `GIVEN native fiat rate non-positive WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) + val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.valueOf(1_000_000_000L), + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoStatus), + ) + coEvery { + currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + } returns nativeCoin + coEvery { + quotesRepository.getMultiQuoteSyncOrNull(setOf(nativeCoin.id.rawCurrencyId!!)) + } returns setOf( + QuoteStatus( + rawCurrencyId = nativeCoin.id.rawCurrencyId!!, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, // non-positive + priceChange = BigDecimal.ZERO, + ), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalArgumentException::class.java) + assertThat(error.message).isEqualTo("Native fiat rate must be > 0") + } + + @Test + fun `GIVEN token fiat rate non-positive WHEN invoke THEN returns error`() = runTest { + val rawNetworkId = Blockchain.BSC.id + val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) + val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal.ZERO) // non-positive + + coEvery { feeRepository.getEthereumFeeWithoutGas(userWalletId, token) } returns Fee.Ethereum.EIP1559( + maxFeePerGas = BigInteger.ONE, + priorityFee = BigInteger.ONE, + gasLimit = BigInteger.ZERO, + amount = BigDecimal.ZERO.convertToSdkAmount( + createStatus(token = token, fiatRate = BigDecimal.ONE), + ), + ) + + val result = useCase(userWalletId, cryptoStatus) + + assertThat(result.isLeft()).isTrue() + val error = (result as Either.Left).value + assertThat(error).isInstanceOf(IllegalArgumentException::class.java) + assertThat(error.message).isEqualTo("Fiat rate for token must be > 0") + } + + private fun createToken(rawNetworkId: String, decimals: Int): CryptoCurrency.Token { + val derivationPath = Network.DerivationPath.None + val network = Network( + id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), + backendId = rawNetworkId, + name = rawNetworkId, + currencySymbol = rawNetworkId.take(3).uppercase(), + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId), + ), + network = network, + name = "TEST_TOKEN", + symbol = "TTK", + decimals = decimals, + iconUrl = null, + isCustom = false, + contractAddress = "0xToken", + ) + } + + private fun createCoin(rawNetworkId: String, decimals: Int): CryptoCurrency.Coin { + val derivationPath = Network.DerivationPath.None + val network = Network( + id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), + backendId = rawNetworkId, + name = rawNetworkId, + currencySymbol = rawNetworkId.take(3).uppercase(), + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + return CryptoCurrency.Coin( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId), + ), + network = network, + name = "TEST_COIN", + symbol = "TCN", + decimals = decimals, + iconUrl = null, + isCustom = false, + ) + } + + private fun createStatus(token: CryptoCurrency.Token, fiatRate: BigDecimal?): CryptoCurrencyStatus { + return CryptoCurrencyStatus( + currency = token, + value = CryptoCurrencyStatus.Custom( + amount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x0000000000000000000000000000000000000000", + type = NetworkAddress.Address.Type.Primary, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index d468e6d60d..e18a49c84a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -283,7 +283,7 @@ internal class YieldSupplyActiveModel @Inject constructor( YieldSupplyActiveFeeContentTransformer( cryptoCurrencyStatus = cryptoStatus, appCurrency = appCurrency, - feeValue = currentFee, + feeValue = currentFee.value, maxNetworkFee = maxFee, analyticsHandler = analyticsHandler, ), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt index 366490ead7..0e5ade929b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt @@ -43,7 +43,6 @@ internal class YieldSupplyNotificationsComponent( Modifier.padding(top = 16.dp) }, containerColor = TangemTheme.colors.background.action, - iconTint = null, ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt index 7b9a1e53ef..446047a107 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/entity/YieldSupplyNotificationData.kt @@ -6,4 +6,5 @@ import java.math.BigDecimal data class YieldSupplyNotificationData( val feeValue: BigDecimal?, val feeError: GetFeeError?, + val shouldShowHighFeeNotification: Boolean = false, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 77907f4839..2e81b51d3f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -74,6 +74,10 @@ internal class YieldSupplyNotificationsModel @Inject constructor( dustValue = null, onReload = params.callback::onFeeReload, ) + + if (data.shouldShowHighFeeNotification) { + add(NotificationUM.Info.YieldSupplyHighNetworkFee) + } } if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index a41f836310..6a2d3d2597 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -190,7 +190,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( updatedTransactionList = updatedTransactionList, feeValue = feeSum, maxNetworkFee = maxFee, - estimatedFeeValueInTokenCurrency = estimatedFee, + estimatedFeeValueInTokenCurrency = estimatedFee.value, minAmount = minAmount, ), ) @@ -198,6 +198,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( data = YieldSupplyNotificationData( feeValue = feeSum, feeError = null, + shouldShowHighFeeNotification = estimatedFee.isHighFee, ), ) }, From 1a9e2006d5505f373570be6bcf80d7ebc38b687b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 14:01:49 +0300 Subject: [PATCH 09/33] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- .../kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 03e0a8cfd3..963f847260 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 @@ -17,7 +17,7 @@ }, { "name": "STAKING_CARDANO_ENABLED", - "version": "undefined" + "version": "5.31.1" }, { "name": "STAKING_ETH_ENABLED", diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt index 4423dddf32..919f80982d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt @@ -1,5 +1,6 @@ package com.tangem.data.pay.util +import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -32,7 +33,7 @@ class TangemPayWalletsManager @Inject constructor( private fun findColdWallet(userWallets: List?): UserWallet.Cold { return userWallets?.find { - it is UserWallet.Cold && it.isMultiCurrency + it is UserWallet.Cold && it.scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable } as? UserWallet.Cold ?: error("Cannot find cold user wallet") } } \ No newline at end of file From 72d32d9254f48fa44afc24e6857b7d1ced24bd7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Dec 2025 09:42:12 +0300 Subject: [PATCH 10/33] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 3 + .../tangem/datasource/di/PromoStoreModule.kt | 9 ++ .../local/promo/DefaultPromoBannerStore.kt | 19 ++++ .../local/promo/PromoBannerStore.kt | 10 ++ .../data/promo/DefaultPromoRepository.kt | 15 +++ .../tangem/data/promo/di/PromoDataModule.kt | 3 + domain/onramp/build.gradle.kts | 1 + .../domain/onramp/GetOnrampOffersUseCase.kt | 35 +++++- .../onramp/GetOnrampOffersUseCaseTest.kt | 105 ++++++++++++++++++ .../tangem/domain/promo/PromoRepository.kt | 2 + tangem-android-tools | 2 +- 11 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0c20fd0993..34ffd4aaba 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides @@ -264,12 +265,14 @@ internal object OnrampDomainModule { onrampErrorResolver: OnrampErrorResolver, onrampTransactionRepository: OnrampTransactionRepository, settingsRepository: SettingsRepository, + promoRepository: PromoRepository, ): GetOnrampOffersUseCase { return GetOnrampOffersUseCase( onrampRepository = onrampRepository, errorResolver = onrampErrorResolver, onrampTransactionRepository = onrampTransactionRepository, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt index b8af7137ae..af846ebcbb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt @@ -1,7 +1,10 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.promo.DefaultPromoBannerStore import com.tangem.datasource.local.promo.DefaultPromoStoriesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import dagger.Module import dagger.Provides @@ -18,4 +21,10 @@ object PromoStoreModule { fun providePromoStoriesStore(): PromoStoriesStore { return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun providePromoBannerStore(): PromoBannerStore { + return DefaultPromoBannerStore(dataStore = RuntimeSharedStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt new file mode 100644 index 0000000000..6065fa079a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore + +internal class DefaultPromoBannerStore( + private val dataStore: RuntimeSharedStore>, +) : PromoBannerStore { + + override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? { + return dataStore.getSyncOrNull()?.get(promoId) + } + + override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) { + dataStore.update(emptyMap()) { current -> + current + (promoId to promoBanner) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt new file mode 100644 index 0000000000..f951238bd5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.PromoBannerResponse + +interface PromoBannerStore { + + suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? + + suspend fun store(promoId: String, promoBanner: PromoBannerResponse) +} \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index d6041cb705..fe58a685da 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStor import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository @@ -27,6 +28,7 @@ internal class DefaultPromoRepository( private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, private val promoStoriesStore: PromoStoriesStore, + private val promoBannerStore: PromoBannerStore, private val dispatchers: CoroutineDispatcherProvider, private val referralRepository: ReferralRepository, ) : PromoRepository { @@ -101,6 +103,18 @@ internal class DefaultPromoRepository( ) } + override suspend fun isMoonpayPromoActive(): Boolean { + val banner = runCatching(dispatchers.io) { + val response = promoBannerStore.getSyncOrNull(MOONPAY_NAME) ?: run { + val apiResponse = tangemApi.getPromoBanner(MOONPAY_NAME).getOrThrow() + promoBannerStore.store(MOONPAY_NAME, apiResponse) + apiResponse + } + promoBannerConverter.convert(response) + }.getOrNull() + return banner?.isActive == true + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } @@ -180,6 +194,7 @@ internal class DefaultPromoRepository( const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" const val ONE_PLUS_ONE_NAME = "one-plus-one" + const val MOONPAY_NAME = "moonpay" const val STORIES_LOAD_DELAY = 1000L } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 69d489a2dc..2f0c689e53 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository import com.tangem.feature.referral.domain.ReferralRepository @@ -23,6 +24,7 @@ internal object PromoDataModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, promoStoriesStore: PromoStoriesStore, + promoBannerStore: PromoBannerStore, dispatchers: CoroutineDispatcherProvider, referralRepository: ReferralRepository, ): PromoRepository { @@ -32,6 +34,7 @@ internal object PromoDataModule { promoStoriesStore = promoStoriesStore, dispatchers = dispatchers, referralRepository = referralRepository, + promoBannerStore = promoBannerStore, ) } } \ No newline at end of file diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index 7823e8dcea..cd1f511092 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.domain.core) api(projects.domain.settings) implementation(deps.kotlin.serialization) + implementation(projects.domain.promo) /** Tests */ testImplementation(deps.test.coroutine) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 0b2aba9380..8bc240224a 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -11,9 +11,11 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class GetOnrampOffersUseCase( @@ -21,14 +23,16 @@ class GetOnrampOffersUseCase( private val onrampTransactionRepository: OnrampTransactionRepository, private val errorResolver: OnrampErrorResolver, private val settingsRepository: SettingsRepository, + private val promoRepository: PromoRepository, ) { operator fun invoke(): EitherFlow> { return combine( onrampRepository.getQuotes(), onrampTransactionRepository.getAllTransactions(), - ) { quotes, transactions -> - processOffers(quotes, transactions) + flow { emit(promoRepository.isMoonpayPromoActive()) }, + ) { quotes, transactions, isMoonpayPromoActive -> + processOffers(quotes, transactions, isMoonpayPromoActive) } .map { offers -> offers.right() } .catch { throwable -> errorResolver.resolve(throwable).left() } @@ -37,6 +41,7 @@ class GetOnrampOffersUseCase( private suspend fun processOffers( quotes: List, transactions: List, + isMoonpayPromoActive: Boolean, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() @@ -57,7 +62,7 @@ class GetOnrampOffersUseCase( val recentOffer = findRecentOffer(offers, transactions) val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) - val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable, isMoonpayPromoActive) return buildOffersBlocks( recentOffer = recentOffer, @@ -85,8 +90,24 @@ class GetOnrampOffersUseCase( return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { - val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } + private fun findFastestOffer( + offers: List, + isGooglePayAvailable: Boolean, + isMoonpayPromoActive: Boolean, + ): OnrampOffer? { + val moonpayPromoOffers = if (isMoonpayPromoActive) { + offers.filter { + it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + } + } else { + emptyList() + } + + val instantOffers = moonpayPromoOffers.ifEmpty { + offers.filter { it.quote.paymentMethod.type.isInstant() } + } + return if (instantOffers.isNotEmpty()) { instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable)) } else { @@ -287,4 +308,8 @@ class GetOnrampOffersUseCase( -> true } } + + private companion object { + const val MOONPAY_PROMO_PROVIDER_ID = "moonpay" + } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 660735a8e7..5f249f97a2 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf @@ -24,6 +25,7 @@ class GetOnrampOffersUseCaseTest { private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val promoRepository: PromoRepository = mockk(relaxUnitFun = true) private lateinit var useCase: GetOnrampOffersUseCase @@ -35,6 +37,7 @@ class GetOnrampOffersUseCaseTest { onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } @@ -225,6 +228,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { promoRepository.isMoonpayPromoActive() } returns false coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf( @@ -245,6 +249,107 @@ class GetOnrampOffersUseCaseTest { } } + @Test + fun `invoke should fallback to standard instant offers when promo is active but no Moonpay offers exist`() = + runTest { + val instantMethod = createMockPaymentMethod("gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val provider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(instantMethod, provider, BigDecimal("95.0")), + createMockQuote(slowMethod, provider, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(2) + + val fastestOffer = + recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isNotEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should show Moonpay fastest offer when promo is active`() = runTest { + val moonpayGooglePayMethod = createMockPaymentMethod("moonpay-gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val otherGooglePayMethod = createMockPaymentMethod( + "other-gpay", + "Other Google Pay", + PaymentMethodType.GOOGLE_PAY, + ) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val moonpayProvider = createMockProvider("moonpay", "Moonpay") + val otherProvider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(moonpayGooglePayMethod, moonpayProvider, BigDecimal("100.0")), + createMockQuote(otherGooglePayMethod, otherProvider, BigDecimal("95.0")), + createMockQuote(slowMethod, otherProvider, BigDecimal("105.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + + val fastestOffer = recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + private fun createMockPaymentMethod( id: String, name: String, diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 18d7a50ec4..db6aa18461 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -19,6 +19,8 @@ interface PromoRepository { suspend fun isMarketsStakingNotificationHideClicked(): Flow suspend fun setMarketsStakingNotificationHideClicked() + + suspend fun isMoonpayPromoActive(): Boolean // endregion // region Stories diff --git a/tangem-android-tools b/tangem-android-tools index 3da4c865da..d7950d60bb 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 3da4c865da5f2a61d5170357c60de4ddd8425815 +Subproject commit d7950d60bb4c6353f2aa3364073c6ec72167c666 From c551b2dea9a4e32b156f29833c6c90087620a082 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 16:12:39 +0200 Subject: [PATCH 11/33] Updated on 2026-08-14 --- .../notifications/AddStakingNotificationsTransformer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 66d3c56eac..5968fa4437 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -291,6 +291,8 @@ internal class AddStakingNotificationsTransformer( } private fun MutableList.addTonInitializeAccountNotification(prevState: StakingUiState) { + if (prevState.actionType !is StakingActionCommonType.Enter) return + val isAccountInitialized = isAccountInitializedProvider.invoke() val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId From 938c8e72cb9b2458d833d1d32252e8c2ac86eb1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Dec 2025 19:45:24 +0500 Subject: [PATCH 12/33] Updated on 2026-08-14 --- .../tangem/data/swap/DefaultSwapRepositoryV2.kt | 4 ++-- .../com/tangem/domain/swap/SwapRepositoryV2.kt | 4 ++-- .../swap/usecase/GetSwapSupportedPairsUseCase.kt | 5 ++++- .../swap/usecase/SwapTransactionSentUseCase.kt | 3 ++- .../confirm/model/SwapTransactionSender.kt | 10 ++++++++++ .../feature/swap/domain/SwapInteractorImpl.kt | 16 ++++++++++++++-- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 343ba4d4bd..56a6d621fb 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -272,7 +272,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( override suspend fun swapTransactionSent( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, - toAddress: String, + payInAddress: String, txId: String, txHash: String, txExtraId: String?, @@ -288,7 +288,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( txId = txId, fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId, fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), - payinAddress = toAddress, + payinAddress = payInAddress, payinExtraId = txExtraId, txHash = txHash, ), diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index d25708c229..8f26ccadb6 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -93,7 +93,7 @@ interface SwapRepositoryV2 { * * @param userWallet selected user wallet * @param fromCryptoCurrencyStatus currency status being swapped from - * @param toAddress swap destination address + * @param payInAddress swap destination address * @param txId transaction id in ExpressApi * @param txHash transaction hash in blockchain * @param txExtraId extra transaction id in ExpressApi @@ -101,7 +101,7 @@ interface SwapRepositoryV2 { suspend fun swapTransactionSent( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, - toAddress: String, + payInAddress: String, txId: String, txHash: String, txExtraId: String?, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index 7cd009b31b..af3d4fe6aa 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -32,7 +32,10 @@ class GetSwapSupportedPairsUseCase( swapTxType = swapTxType, ) - val filteredOutInitial = cryptoCurrencyList.filterNot { it.id.rawNetworkId == initialCurrency.id.rawNetworkId } + val filteredOutInitial = cryptoCurrencyList.filterNot { currency -> + currency.id.rawNetworkId == initialCurrency.id.rawNetworkId && + currency.id.rawCurrencyId == initialCurrency.id.rawCurrencyId + } val fromGroup = pairs.groupPairs( initialCurrency = initialCurrency, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index be872f8701..92fcc325e3 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -27,6 +27,7 @@ class SwapTransactionSentUseCase( swapDataTransactionModel: SwapDataTransactionModel, provider: ExpressProvider, txHash: String, + payInAddress: String, timestamp: Long, swapTxType: SwapTxType, ) = Either.catch { @@ -63,7 +64,7 @@ class SwapTransactionSentUseCase( swapRepositoryV2.swapTransactionSent( userWallet = userWallet, fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, - toAddress = swapDataTransactionModel.txTo, + payInAddress = payInAddress, txId = swapDataTransactionModel.txId, txHash = txHash, txExtraId = swapDataTransactionModel.txExtraId, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 3602881267..1953adbd19 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -1,7 +1,9 @@ package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProvider @@ -152,6 +154,13 @@ internal class SwapTransactionSender @AssistedInject constructor( return } + val ethereumCallData = (txData.extras as? EthereumTransactionExtras)?.callData + val payInAddress = if (ethereumCallData is EthereumYieldSupplySendCallData) { + ethereumCallData.destinationAddress + } else { + txData.destinationAddress + } + sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -169,6 +178,7 @@ internal class SwapTransactionSender @AssistedInject constructor( swapDataTransactionModel = swapTransaction, provider = provider, txHash = txHash, + payInAddress = payInAddress, timestamp = timestamp, swapTxType = SwapTxType.SendWithSwap, ) 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 aa8415a3c9..0531735446 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 @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain import android.util.Base64 import arrow.core.Either import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain @@ -11,6 +12,7 @@ import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId @@ -870,6 +872,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } + return handleSwapResult( provider = provider, networkId = networkId, @@ -881,7 +884,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount, derivationPath = derivationPath, txData = txData, - payInAddress = txData.destinationAddress, + payInAddress = getPayoutAddress(txData), ) } @@ -1087,7 +1090,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txId = exchangeDataCex.txId, fromNetwork = currencyToSend.currency.network.backendId, fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), - payInAddress = txData.destinationAddress, + payInAddress = getPayoutAddress(txData), txHash = txHash, payInExtraId = exchangeDataCex.txExtraId, ) @@ -2307,6 +2310,15 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } + private fun getPayoutAddress(txData: TransactionData.Uncompiled): String { + val ethereumCallData = (txData.extras as? EthereumTransactionExtras)?.callData + return if (ethereumCallData is EthereumYieldSupplySendCallData) { + ethereumCallData.destinationAddress + } else { + txData.destinationAddress + } + } + companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% From 8c4f39aab53472381d22924ad962dd379ea23688 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 15:16:40 +0300 Subject: [PATCH 13/33] Updated on 2026-08-14 --- .../com/tangem/domain/apptheme/model/AppThemeMode.kt | 10 +++++----- features/wallet/impl/build.gradle.kts | 2 ++ .../feature/wallet/child/wallet/model/WalletModel.kt | 11 +++++++++-- .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 9 ++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt index dd24287e13..85da14ca89 100644 --- a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -3,21 +3,21 @@ package com.tangem.domain.apptheme.model /** * Enumerates the possible modes for the application's theme. */ -enum class AppThemeMode { +enum class AppThemeMode(val value: String) { /** * Forces the dark theme mode regardless of system settings. */ - FORCE_DARK, + FORCE_DARK("Dark"), /** * Forces the light theme mode regardless of system settings. */ - FORCE_LIGHT, + FORCE_LIGHT("Light"), /** * Follows the system-wide theme mode. */ - FOLLOW_SYSTEM, + FOLLOW_SYSTEM("System"), ; @@ -30,6 +30,6 @@ enum class AppThemeMode { /** * List of available [AppThemeMode]s. * */ - val available: List = values().toList() + val available: List = entries } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index ca7ea45cc6..7949e74246 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -103,6 +103,8 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply.models) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) /** Feature Apis */ implementation(projects.features.details.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2338f56503..4f4b296a7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,6 +10,8 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -96,6 +98,7 @@ internal class WalletModel @Inject constructor( private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -106,14 +109,18 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() - private var needToRefreshWallet = false private val clearNFTCacheJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() + private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + modelScope.launch { + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(theme.value)) + } screenLifecycleProvider.isBackgroundState .onEach { isBackground -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 54d3a4cca2..36cca3a129 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -51,7 +51,14 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - data object ScreenOpened : MainScreen(event = "Screen opened") + class ScreenOpened( + val theme: String, + ) : MainScreen( + event = "Screen opened", + params = mapOf( + "App Theme" to theme, + ), + ) class WalletSelected(val isImported: Boolean) : MainScreen( event = "Wallet Selected", From aa11b11fee8dcd6078c9cc663ddef51f2d61a8d6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Dec 2025 21:24:20 +0400 Subject: [PATCH 14/33] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 5 +- core/res/src/main/res/values-es/strings.xml | 9 ++- core/res/src/main/res/values-fr/strings.xml | 8 ++- core/res/src/main/res/values-it/strings.xml | 8 ++- core/res/src/main/res/values-ja/strings.xml | 49 +++++++------ core/res/src/main/res/values-ru/strings.xml | 17 +++-- .../src/main/res/values-uk-rUA/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 71 ++++++++++--------- 9 files changed, 101 insertions(+), 71 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 0dc6af1380..af3e4fd8d7 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -392,6 +392,7 @@ Woche mit Ja + Ertragsmodus Vertragsadresse kopiert! Verfügbare Netzwerke Die Ableitung Deines Tokens entspricht der Ableitung von %1$s. Dein Token wird diesem Konto gutgeschrieben. @@ -1483,11 +1484,11 @@ Ein separates Zahlungskonto wird erstellt, ohne Ihre Adressen und Vermögenswerte offenzulegen Unerreichte Privatsphäre Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten - Payment account + Zahlungskonto Synchronisierung des Zahlungskontos erforderlich Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut. Service vorübergehend nicht verfügbar - Der Dienst ist derzeit nicht erreichbar. Bitte versuchen Sie es später erneut. + Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. Synchronisation erforderlich Tangem Visa Card Tangem Pay ist vorübergehend nicht verfügbar diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b0aabad52d..e99920eeb5 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -345,6 +345,7 @@ semana con + Modo de rendimiento Dirección de contrato copiada Redes disponibles Dirección de contrato @@ -1319,11 +1320,11 @@ Se creará una cuenta de pago separada sin divulgar tus direcciones y activos Privacidad inigualable Obtén tu tarjeta Tangem Pay gratuita en minutos - Payment account + Cuenta de pago Sincronización de cuenta de pago necesaria Estamos solucionando un problema técnico. Por favor, inténtelo de nuevo más tarde. Servicio temporalmente no disponible - El servicio no está disponible actualmente. Inténtalo de nuevo más tarde. + No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando. Sincronización necesaria Tangem Visa Card Tangem Pay temporalmente no disponible @@ -1417,6 +1418,10 @@ Renombrar la billetera Desbloquear todo Desbloquear todo con %s + Configure un código de 4 dígitos.\nSe utilizará para los pagos. + Crear código PIN + No se aceptó el PIN. Inténtalo de nuevo o usa un código diferente. + PIN no válido: evitar secuencias o repeticiones Elija cómo agregar su billetera La blockchain está Inaccesible. Inténtelo más tarde Escanee la tarjeta o el anillo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index ca122acb3d..faddf299c6 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -328,6 +328,7 @@ semaine avec Oui + Mode Rendement Adresse du contrat copiée ! Réseaux disponibles Adresse du contrat @@ -1300,11 +1301,11 @@ Un compte de paiement séparé sera créé sans divulguer vos adresses et actifs Confidentialité inégalée Obtenez votre carte Tangem Pay gratuite en quelques minutes - Payment account + Compte de paiement Synchronisation du compte de paiement nécessaire Nous réparons un problème technique. Veuillez réessayer plus tard. Service temporairement indisponible - Le service est actuellement indisponible. Veuillez réessayer plus tard. + Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours. Synchronisation requise Tangem Visa Card Tangem Pay est temporairement indisponible @@ -1408,6 +1409,9 @@ Cela ne prendra pas longtemps. Nous configurons votre compte. Cela ne prendra pas longtemps. Nous terminons l\'activation. Tout est en cours de préparation ! + Créez un code à 4 chiffres. Il servira pour les paiements. + Créer un code PIN + Le code PIN n\'a pas été accepté. Veuillez réessayer ou utiliser un autre code. Code PIN invalide : évitez les séquences ou les répétitions Accéder le site Web Continuons la configuration de votre compte. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 179f7018a0..b7536b13ca 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -162,11 +162,11 @@ Verrà creato un conto di pagamento separato senza divulgare i tuoi indirizzi e asset Privacy senza rivali Ottieni la tua carta Tangem Pay gratuita in pochi minuti - Payment account + Conto di pagamento Sincronizzazione del conto di pagamento necessaria Stiamo risolvendo un problema tecnico. Riprova più tardi. Servizio temporaneamente non disponibile - Il servizio è attualmente non raggiungibile. Riprova più tardi. + Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare. Sincronizzazione necessaria Tangem Visa Card Tangem Pay è temporaneamente non disponibile @@ -174,6 +174,10 @@ Usa la tua carta o il tuo anello per ripristinare l\'accesso al tuo conto di pagamento Il tuo codice PIN Tangem Twin + Imposta un codice di 4 cifre.\nVerrà utilizzato per i pagamenti. + Crea codice PIN + Il PIN non è stato accettato. Riprova o utilizza un codice diverso. + PIN non valido: evitare sequenze o ripetizioni WalletConnect L\'indirizzo è stato copiato con successo Nessuna connessione a Internet diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 82aca73289..bce005b585 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -4,7 +4,7 @@ とにかくスキップ アクセスコードが設定されていません コードを変更 - アクセスコードは、ウォレットのロック解除・資産へのアクセス保護に使用されます + アクセスコードは、ウォレットのロック解除およびウォレットへのアクセス保護に使用されます このまま使う このアクセスコードは簡単に推測される可能性があります アクセスコードを入力 @@ -96,7 +96,7 @@ %1$s ネットワークのトークンは、ファームウェアの制限により、このカードまたはリングではサポートされていません。 カードまたはリングのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 - まずアクセスコードを設定してから、生体認証を有効にしてください + 生体認証を有効にするには、アクセスコードを設定してください %1$sを使用してウォレットのロックを解除し、取引の署名などの機密性の高い操作を承認してください。ハードウェアウォレットの場合は、署名にカードまたはリングが必要です。 デフォルト手数料 デフォルト手数料を有効にすると、取引手数料が自動的に設定され、送金時に手数料ページを表示する必要がなくなります。必要に応じて、いつでもこのページに戻ることができます。 @@ -136,7 +136,7 @@ リカバリーフレーズ これらの単語は誰にも教えないでください。Tangemがこれらの単語を尋ねることは、絶対にありません。以下の%s個の単語はウォレットのリカバリーフレーズです。デバイスを紛失した場合、これらの単語を使用してウォレットを復元できます。 これらの%s個の単語を番号順に書き留め、安全かつ他人の目に触れない場所に保管してください - ウォレットおよびリカバリーフレーズの保護とバックアップは、すべてあなた自身の責任となります。 + ウォレットの安全確保およびリカバリーフレーズの安全なバックアップは、すべてご自身の責任となります。 リカバリーフレーズ 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 今後表示しない @@ -386,6 +386,7 @@ はい + 利息モード コントラクトアドレスをコピーしました! 利用可能なネットワーク このトークンの導出パスが%1$sの導出パスと一致しています。トークンはこのアカウントに追加されます。 @@ -575,8 +576,8 @@ 今すぐ確定 ウォレットの設定を完了する アクセスコードでアプリを保護して、設定を完了してください。 - そうした場合は、最初からやり直す必要があります。 - 本当にアクティベーション処理を終了してもよろしいですか? + 終了すると、最初からやり直す必要があります。 + セットアップを中止してもよろしいですか? 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する @@ -585,18 +586,19 @@ 新しいウォレットを作成 Tangemの高性能ハードウェアウォレットで、セキュリティをさらに強化しましょう。 ハードウェアウォレット - 現在のウォレットをTangemウォレットに移します。 + 現在のウォレットをTangemに移します。 現在のウォレットをアップグレードする バックアップへ移動 アクセスコードを作成する前にウォレットをバックアップしてください。 - まずバックアップを完了する + 先にバックアップを完了してください まずバックアップを完了する その他の方法 - リカバリーフレーズは、ご自身で安全な場所に保管し、資金を守るために他人には絶対に共有しないでください。 + 資金を保護するため、リカバリーフレーズは安全な場所に保管し、他人に知られないようにしてください。 リカバリーフレーズ - アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 - ウォレットをハードウェアにアップグレードするには、まずバックアップしてください。 - 鍵はアプリに保存されます + アクセスコードでウォレットを保護するには、バックアップの手続きを完了してください。 + ハードウェアウォレットにアップグレードするには、バックアップの手続きを完了してください。 + 秘密鍵はデバイス上に保持されます + リカバリーフレーズを使ってウォレットを作成または復元してください。 シードフレーズのバックアップ モバイルウォレットを作成する 既存のウォレットをインポートする @@ -610,25 +612,25 @@ バックアップを表示 ウォレット削除 とにかく削除する - このウォレットにはバックアップが存在します。後で復元できるように、削除する前に内容を確認してください。 - バックアップなしでこのウォレットを削除すると、資金へのアクセスを永久に失います。 - このウォレットを削除しますか? - ウォレットの削除前にバックアップをしていなければ、アクセスを失う可能性があることを理解しています。 - ウォレットを削除しても中身自体が消えるわけではなく、単にこのデバイスから表示が消えるだけであることを理解しています。 - もはや、シードフレーズは不要。Tangemカードやリングが、そのまま安全なバックアップになります。 + このウォレットにはバックアップがあります。ウォレットを削除する前に、復元できることを確認してください。 + バックアップを行わずにこのウォレットを削除すると、資金へのアクセスを永久に失います。 + このウォレットを削除してもよろしいですか? + ウォレットを削除する前にバックアップを行っていない場合、ウォレットへのアクセスを失うことを理解しています。 + ウォレットを削除しても中身自体が消えるわけではなく、このデバイスから表示が消えるだけであることを理解しています。 + シードフレーズは不要です。Tangemカードまたはリングが安全なバックアップとなります。 Tangemでバックアップ アップグレードできません。このデバイスにはすでにウォレットが存在します。 別のデバイスを選択してください。このデバイスはアップグレードに使用できません。 操作中にエラーが発生しました。 処理中も、資金は安全に保たれ、常にアクセス可能です。 資金へのアクセス - ウォレットのデータはアプリから削除され、Tangemデバイスに保存されます。 + ウォレットのデータはアプリから削除され、ハードウェアウォレットに保存されます セキュリティ全般 - 秘密鍵はアプリからTangemデバイスに移動されます。 - 鍵の移行 + 秘密鍵はアプリからTangemハードウェアウォレットへ移行されます + キーの移行 デバイスをスキャン アップグレードを開始 - ハードウェアウォレットにアップグレードすると、資産がコールドストレージで安全に保管されます。 + ハードウェアウォレットへのアップグレードを行います。資産はコールドストレージで安全に保管されます。 Tangemウォレット ハードウェアウォレットにアップグレード Tangemの業界最高クラスのハードウェアウォレットで、暗号資産を安全に保管しましょう。 @@ -714,6 +716,7 @@ 上昇率上位 下落率上位 トレンド + 利息モード ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s 最大%s APYを獲得 トークンを追加しました @@ -963,6 +966,7 @@ その他の通貨 人気の法定通貨 通貨で検索 + この取引はすでに処理されています。これ以上の対応は必要ありません。 最良のレートを取得しています... 即時 オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします @@ -1481,7 +1485,7 @@ 支払いアカウントの同期が必要です 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません - 現在サービスに接続できません。後ほどもう一度お試しください。 + 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 同期が必要です Tangem Visaカード Tangem Payは一時的に利用できません @@ -1589,6 +1593,7 @@ 長くはかかりません。アクティベーションを完了しています。 準備完了です! 4桁のコードを設定します。 \nお支払いの際に使用されます。 + PINコードを作成する PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください ウェブサイトに移動 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 4b22469076..0d5eaa197f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -402,6 +402,7 @@ неделю с Да + Режим доходности Адрес контракта скопирован! Доступные сети Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи. @@ -1450,7 +1451,7 @@ Разморозить карту? Не удалось разморозить карту, попробуйте еще раз Карта разморожена - Вывод + Вывести Пополнить Способы пополнения Номер @@ -1486,7 +1487,7 @@ Пополните карту любым активом через обмен Реквизиты Разморозить карту - Вывод + Вывести Вывод сейчас недоступен Вы не можете начать обмен или новый вывод, пока не завершится текущий. Вывод выполняется @@ -1518,16 +1519,16 @@ Мы создадим отдельный платежный счет без раскрытия ваших активов \nи их адресов Абсолютная приватность Откройте виртуальную \nTangem Pay Card - Payment account - Требуется синхронизация платежного счета + Платежный аккаунт + Требуется синхронизация платежного аккаунта Мы устраняем техническую проблему. Пожалуйста, попробуйте позже. Сервис временно недоступен - Сервис временно недоступен. Пожалуйста, попробуйте позже. + Не можем показать данные карты, но оплаты продолжают работать. Требуется синхронизация Tangem Visa Card Tangem Pay временно недоступен Tangem Pay - Используйте вашу карту или кольцо для восстановления доступа к платежному счету + Используйте вашу карту или кольцо для восстановления доступа к платежному аккаунту Ваш PIN-код Это мой кошелек Балансы скрыты @@ -1621,7 +1622,11 @@ Разблокировать все с %s Код доступа Активация аккаунта + Установите 4-значный код.\nОн будет использоваться для платежей. + ПИН-код + Установить ПИН-код ПИН не принят. Попробуйте ещё раз или введите другой код. + Слабый ПИН: не используйте повторы или последовательности. Разблокировать Выберите способ добавления кошелька Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 98ddcc642a..92eff0a832 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -257,6 +257,7 @@ тиждень з Так + Режим дохідності Адреса контракту скопійована! Доступні мережі Адреса контракту diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b2f04957dd..6fa2c8b69c 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -405,11 +405,11 @@ 將創建單獨的支付帳戶,且不會透露您的地址和資產 無與倫比的隱私 在幾分鐘內獲得免費的 Tangem Pay 卡 - Payment account + 付款帳戶 需要同步支付账户 我们正在修复技术问题。请稍后再试。 服務暫時無法使用 - 服务当前不可用,请稍后再试 + 目前無法顯示資料,但卡片支付仍可正常使用。 需要同步 Tangem Visa Card Tangem Pay暂时不可用 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8c52e119ff..d365c6e247 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ Skip anyway Access code not set Change code - Your access code will be used to unlock your wallet and to protect access to the assets + Your access code unlocks and protects access to your wallet Use anyway This access code can be easily guessed Enter access code @@ -96,7 +96,7 @@ Tokens in %1$s network are not supported by this card or ring due to firmware limitation. Are you having difficulty scanning your card or ring? This card is not designed to work with this app - Set an access code first to enable biometrics + Set an access code to enable biometrics Use %1$s to unlock your wallet and approve sensitive actions, like signing transactions. For hardware wallets, a card or ring is still required to sign. Default Fee Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. @@ -137,7 +137,7 @@ Recovery phrase Never share these words with anyone. Tangem will never ask you for them. The %s words below are your wallet\'s recovery phrase. Use them to restore your wallet if you lose your device. Write down these %s words in numerical order and keep them safe and private - You are fully responsible for securing and backing up your wallet and recovery phrase. + You are fully responsible for securing your wallet and safely backing up your recovery phrase. Recovery phrase To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again @@ -394,6 +394,7 @@ week with Yes + Yield Mode Contract address copied! Available networks Derivation of your token matches the derivation of %1$s. Your token will be added to this account. @@ -583,31 +584,31 @@ Finalize now Finalize wallet setup Complete setup by securing the app with an access code. - If you do, you\'ll need to start over. - Are you sure you want to quit the activation process? - Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. + If you exit, you\'ll need to start over. + Are you sure you want to quit the setup process? + Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup Google Drive backup - Create a new secure wallet and transfer your funds for extra protection. + Create a secure wallet and transfer your funds for extra protection. Create new wallet Level up your security with the superior Tangem hardware wallet. - Hardware Wallet - Move your current wallet into Tangem Wallet. + Hardware wallet + Move your current wallet into Tangem. Upgrade current wallet Go to backup Please back up your wallet before creating an access code. - Finish Backup First + Finish backup first Finalize backup first Incomplete Other methods - Manually save your recovery phrase in a secure place and keep it private to protect your funds. + Save your recovery phrase in a secure place and keep it private to protect your funds. Recovery phrase - To secure your wallet with an Access Code, complete the backup first. - To upgrade your wallet to hardware, back it up first. + To secure your wallet with an access code, complete the backup process. + To upgrade to a hardware wallet, complete the backup process. Your private keys are securely encrypted and stored on your phone - Keys are stored in the app - Create or restore your wallet using a recovery phrase — your built-in backup. + Private keys stay on your device + Create or restore your wallet with a recovery phrase. Seed phrase backup Create Mobile Wallet Import existing wallet @@ -620,28 +621,28 @@ Go to backup View backup Forget wallet - Forget Anyway - A backup for this wallet exists. Review it before forgetting to make sure you can recover later. - If you forget this wallet without a backup, you’ll permanently lose access to your funds. - Forget this wallet? - I understand that if I haven\'t backed up my wallet before removing it, I may lose access to it. - I understand that removing my wallet does not delete it—but simply removes it from my device. - No seed phrase is needed anymore. Your Tangem card or ring becomes your secure backup. + Forget anyway + This wallet has a backup. Ensure you can recover it before forgetting the wallet. + If you forget this wallet without a backup, you\'ll permanently lose access to your funds. + Are you sure you want to forget this wallet? + I understand that if I haven\'t backed up my wallet before removing it, I will lose access to it. + I understand that removing my wallet does not delete it, only removes it from my device. + Seed phrase not required. Your Tangem card or ring becomes your secure backup. Backup with Tangem - Can’t upgrade. A wallet already exists on this device. + Can\'t upgrade. A wallet already exists on this device. Pick another device. This one can’t be used for the upgrade. An error occurred during the operation. Your funds remain safe and fully accessible during the process Access to funds - Your wallet data will be erased from the app and stored on your Tangem device + Your wallet data will be erased from the app and stored on your hardware wallet General security - Private keys will be moved from the app to your Tangem device - Key Migration + Private keys will be moved from the app to your Tangem hardware wallet + Key migration Scan device Start upgrade - You’re about to upgrade to our hardware wallet. This will keep your assets safe in cold storage. + You\'re about to upgrade to our hardware wallet. It will keep your assets safe in cold storage. Tangem Wallet - Upgrade to Hardware Wallet + Upgrade to our Hardware Wallet Keep your crypto safe with Tangem\'s top-tier hardware wallet. Upgrade your wallet to hardware security This information was generated with AI.\nTap here, if you find any errors. @@ -726,6 +727,7 @@ Top Gainers Top Losers Trending + Yield Mode Staking is the easiest way to receive rewards on your crypto. %s Earn up to %s APY Token Added @@ -801,7 +803,7 @@ Mobile Wallet requires %1$s or later All news - + %dh ago %dh ago @@ -983,6 +985,7 @@ Other currencies Popular Fiats Search by currency + This transaction has already been processed. No further action is required. Fetching best rates... Instant By using onramp functionality, you agree with provider’s %1$s and %2$s @@ -1435,6 +1438,7 @@ Failed to unfreeze the card. Try again later. Your card is unfrozen. Withdrawal + Unable to use on rooted device Add funds Top-up options Card Number @@ -1476,6 +1480,7 @@ Withdrawal in progress Change PIN-code Come back to the app if you forget it. + I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. @@ -1506,10 +1511,10 @@ Payment account sync needed We’re fixing a technical issue. Please try again later. Service temporarily unavailable - The service is currently unreachable. Please try again later. + Unable to display details. However, card payments are still working. Sync needed Tangem Visa Card - Tangem Pay is temporarily unavailable + Tangem Pay is temporarily unreachable Tangem Pay Use your card or ring to restore access to your payment account Your PIN code @@ -1727,7 +1732,7 @@ Using a Tangem Wallet already? Scan now Pick a wallet setup method - Want to purchase Tangem Wallet? + Want to buy a Tangem Wallet? Buy now Recover existing wallet via Google Drive backup Import from Google Drive @@ -1957,7 +1962,7 @@ Create or import a software wallet on your phone. Start with Mobile Wallet Other method - Use Tangem Hardware Wallet + Use a Tangem hardware wallet Learn more & buy Discard You have an interrupted backup. Do you want to resume? From 1e8890dac42b25aafa3b6d4eb1df79a47ccd1f0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 12:59:21 +0400 Subject: [PATCH 15/33] Updated on 2026-08-14 --- .../YieldSupplyGetRewardsBalanceUseCase.kt | 3 ++ ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 32 +++++++++++++++++++ .../TokenDetailsLoadedBalanceConverter.kt | 12 +++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index 93e18928bf..f0b5c39b43 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -29,6 +29,9 @@ class YieldSupplyGetRewardsBalanceUseCase( val fiatRate = status.value.fiatRate if (cryptoAmount?.compareTo(BigDecimal.ZERO) == 0) { + emit( + YieldSupplyRewardBalance(fiatBalance = null, cryptoBalance = null), + ) return@flow } diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index c086813f73..d8b29bfd94 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -49,6 +49,38 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { assertThat(emissions).isEmpty() } + @Test + fun `GIVEN zero amount WHEN invoke THEN emit null balances`() = runTest { + val network = createNetwork() + val token = createToken(network) + val status = CryptoCurrencyStatus( + currency = token, + value = CryptoCurrencyStatus.Custom( + amount = BigDecimal.ZERO, + fiatAmount = null, + fiatRate = BigDecimal.ONE, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + val dispatcherProvider = testDispatcherProvider(this) + val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider) + val appCurrency = AppCurrency.Default + + val emissions = useCase(status, appCurrency).toList() + assertThat(emissions).hasSize(1) + assertThat(emissions[0].fiatBalance).isNull() + assertThat(emissions[0].cryptoBalance).isNull() + } + @Test fun `GIVEN coin currency WHEN invoke THEN emit nothing`() = runTest { val network = createNetwork() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 306315e766..908fdf6c7e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -98,8 +98,16 @@ internal class TokenDetailsLoadedBalanceConverter( stakingCryptoAmount, currentState.selectedBalanceType, ), - displayYieldSupplyFiatBalance = (currentState as? TokenDetailsBalanceBlockState.Content) - ?.displayYieldSupplyFiatBalance, + displayYieldSupplyFiatBalance = if (status.value.yieldSupplyStatus?.isActive == true) { + (currentState as? TokenDetailsBalanceBlockState.Content)?.displayYieldSupplyFiatBalance + } else { + null + }, + displayYieldSupplyCryptoBalance = if (status.value.yieldSupplyStatus?.isActive == true) { + (currentState as? TokenDetailsBalanceBlockState.Content)?.displayYieldSupplyCryptoBalance + } else { + null + }, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, From 0e85eb1d78c70868e1da41cdaa9d3f279f9713e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 13:19:18 +0300 Subject: [PATCH 16/33] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 28 ++- .../tangem/feature/swap/model/SwapModel.kt | 187 ++++++++++++------ 2 files changed, 155 insertions(+), 60 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 8d8ed4d072..96e04a99e6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -43,18 +43,31 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + data class ButtonGivePermissionClicked( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Button - Give permission", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) data class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, + val provider: SwapProvider, ) : SwapEvents( event = "Button - Permission Approve", params = mapOf( "Send Token" to sendToken, "Receive Token" to receiveToken, "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited", + "Provider" to provider.name, ), ) @@ -141,4 +154,17 @@ sealed class SwapEvents( data object Recommended : PromoState("Recommended") } } + + data class NoticePermissionNeeded( + val sendToken: String, + val receiveToken: String, + val provider: SwapProvider, + ) : SwapEvents( + event = "Notice - Permission Needed", + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Provider" to provider.name, + ), + ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d227b21dfd..649b97059f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -189,6 +189,7 @@ internal class SwapModel @Inject constructor( private val toTokenBalanceJobHolder = JobHolder() private var isAmountChangedByUser: Boolean = false + private var lastPermissionNotificationTokens: Pair? = null val currentScreen: SwapNavScreen get() = swapRouter.currentScreen @@ -544,60 +545,101 @@ internal class SwapModel @Inject constructor( private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { when (state) { is SwapState.QuotesLoadedState -> { - fillLoadedDataState(state, state.permissionState, state.swapDataModel) - val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() - val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId - uiState = stateBuilder.createQuotesLoadedState( - uiStateHolder = uiState, - quoteModel = state, - fromToken = fromToken.currency, - feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, - swapProvider = provider, - bestRatedProviderId = bestRatedProviderId, - isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, - selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - hideFee = tangemPayInput?.isWithdrawal == true, - ) - if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { - analyticsEventHandler.send( - SwapEvents.NoticeNotEnoughFee( - token = initialCurrencyFrom.symbol, - blockchain = fromToken.currency.network.name, - ), - ) - } + setupQuotesLoadedUiState(provider, state, fromToken) + sendAnalyticsForNotifications(fromToken) + updatePermissionNotificationState(state) } is SwapState.EmptyAmountState -> { - val toTokenStatus = dataState.toCryptoCurrency - uiState = stateBuilder.createQuotesEmptyAmountState( - uiStateHolder = uiState, - emptyAmountState = state, - fromTokenStatus = fromToken, - toTokenStatus = toTokenStatus, - isReverseSwapPossible = isReverseSwapPossible(), - toAccount = dataState.toAccount, - ) + setupEmptyAmountUiState(state, fromToken) + lastPermissionNotificationTokens = null } is SwapState.SwapError -> { - singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesErrorState( - uiStateHolder = uiState, - swapProvider = provider, - fromToken = state.fromTokenInfo, - toToken = dataState.toCryptoCurrency, - expressDataError = state.error, - includeFeeInAmount = state.includeFeeInAmount, - isReverseSwapPossible = isReverseSwapPossible(), - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - toAccount = dataState.toAccount, - ) - sendErrorAnalyticsEvent(state.error, provider) + setupErrorUiState(provider, state) + lastPermissionNotificationTokens = null } } } + private fun setupQuotesLoadedUiState( + provider: SwapProvider, + state: SwapState.QuotesLoadedState, + fromToken: CryptoCurrencyStatus, + ) { + fillLoadedDataState(state, state.permissionState, state.swapDataModel) + val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId + uiState = stateBuilder.createQuotesLoadedState( + uiStateHolder = uiState, + quoteModel = state, + fromToken = fromToken.currency, + feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, + swapProvider = provider, + bestRatedProviderId = bestRatedProviderId, + isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, + selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + hideFee = tangemPayInput?.isWithdrawal == true, + ) + } + + private fun sendAnalyticsForNotifications(fromToken: CryptoCurrencyStatus) { + if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { + analyticsEventHandler.send( + SwapEvents.NoticeNotEnoughFee( + token = initialCurrencyFrom.symbol, + blockchain = fromToken.currency.network.name, + ), + ) + } + } + + private fun updatePermissionNotificationState(state: SwapState.QuotesLoadedState) { + val fromTokenId = state.fromTokenInfo.cryptoCurrencyStatus + .currency.id.value + val toTokenId = state.toTokenInfo.cryptoCurrencyStatus + .currency.id.value + val currentTokenPair = Pair(fromTokenId, toTokenId) + + when { + uiState.notifications.none { it is SwapNotificationUM.Info.PermissionNeeded } -> { + lastPermissionNotificationTokens = null + } + lastPermissionNotificationTokens != currentTokenPair -> { + sendNoticePermissionNeededEvent() + lastPermissionNotificationTokens = currentTokenPair + } + } + } + + private fun setupEmptyAmountUiState(state: SwapState.EmptyAmountState, fromToken: CryptoCurrencyStatus) { + val toTokenStatus = dataState.toCryptoCurrency + uiState = stateBuilder.createQuotesEmptyAmountState( + uiStateHolder = uiState, + emptyAmountState = state, + fromTokenStatus = fromToken, + toTokenStatus = toTokenStatus, + isReverseSwapPossible = isReverseSwapPossible(), + toAccount = dataState.toAccount, + ) + } + + private fun setupErrorUiState(provider: SwapProvider, state: SwapState.SwapError) { + singleTaskScheduler.cancelTask() + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + expressDataError = state.error, + includeFeeInAmount = state.includeFeeInAmount, + isReverseSwapPossible = isReverseSwapPossible(), + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + toAccount = dataState.toAccount, + ) + sendErrorAnalyticsEvent(state.error, provider) + } + private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { val receiveToken = dataState.toCryptoCurrency?.currency?.let { "${it.network.backendId}:${it.symbol}" @@ -1331,7 +1373,7 @@ internal class SwapModel @Inject constructor( onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() - analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + sendGivePermissionClickedEvent() uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) @@ -1579,19 +1621,46 @@ internal class SwapModel @Inject constructor( }.map { it.currency }.contains(chosen.currency) } + private fun sendNoticePermissionNeededEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.NoticePermissionNeeded( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + + private fun sendGivePermissionClickedEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val provider = dataState.selectedProvider ?: return + analyticsEventHandler.send( + SwapEvents.ButtonGivePermissionClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + provider = provider, + ), + ) + } + private fun sendPermissionApproveClickedEvent() { - val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol - val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol - val approveType = uiState.permissionState.getApproveTypeOrNull() - if (sendTokenSymbol != null && receiveTokenSymbol != null && approveType != null) { - analyticsEventHandler.send( - SwapEvents.ButtonPermissionApproveClicked( - sendToken = sendTokenSymbol, - receiveToken = receiveTokenSymbol, - approveType = approveType, - ), - ) - } + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return + val approveType = uiState.permissionState.getApproveTypeOrNull() ?: return + val provider = dataState.selectedProvider ?: return + + analyticsEventHandler.send( + SwapEvents.ButtonPermissionApproveClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + approveType = approveType, + provider = provider, + ), + ) } private fun updateWalletBalance() { From 929dae92c388cb1c472fc981894fe5152806413c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 13:51:47 +0300 Subject: [PATCH 17/33] Updated on 2026-08-14 --- .../tangem/domain/models/wallet/UserWallet.kt | 7 ++++++ .../wallet/child/wallet/model/WalletModel.kt | 24 ++++++++++++------- .../analytics/WalletScreenAnalyticsEvent.kt | 2 ++ .../utils/SelectedWalletAnalyticsSender.kt | 8 +------ 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt index 30b9500769..8f2b0572ff 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt @@ -86,6 +86,13 @@ fun UserWallet.requireHotWallet(): UserWallet.Hot { ?: error("This user wallet is not a hot wallet") } +fun UserWallet.isImported(): Boolean { + return when (this) { + is UserWallet.Cold -> isImported + is UserWallet.Hot -> true + } +} + fun UserWallet.copy(name: String = this.name, walletId: UserWalletId = this.walletId): UserWallet = when (this) { is UserWallet.Cold -> this.copy(name = name, walletId = walletId) is UserWallet.Hot -> this.copy(name = name, walletId = walletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 4f4b296a7d..43aac5ae23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isImported import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase @@ -116,12 +117,6 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - modelScope.launch { - val result = getAppThemeModeUseCase().firstOrNull() - val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(theme.value)) - } - screenLifecycleProvider.isBackgroundState .onEach { isBackground -> if (isBackground.not()) { @@ -278,8 +273,8 @@ internal class WalletModel @Inject constructor( // It's okay here because we need to be able to observe the selected wallet changes @Suppress("DEPRECATION") private fun subscribeOnSelectedWalletFlow() { - getSelectedWalletUseCase().onRight { - it + getSelectedWalletUseCase().onRight { walletFlow -> + walletFlow .conflate() .distinctUntilChanged() .onEach { selectedWallet -> @@ -289,6 +284,8 @@ internal class WalletModel @Inject constructor( subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) + + sendMainScreenOpenedAnalytics(selectedWallet.isImported()) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -698,6 +695,17 @@ internal class WalletModel @Inject constructor( } } + private suspend fun sendMainScreenOpenedAnalytics(isImported: Boolean) { + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + theme = theme.value, + isImported = isImported, + ), + ) + } + inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 36cca3a129..3ecfa644f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -53,10 +53,12 @@ sealed class WalletScreenAnalyticsEvent { class ScreenOpened( val theme: String, + val isImported: Boolean, ) : MainScreen( event = "Screen opened", params = mapOf( "App Theme" to theme, + "Wallet Type" to if (isImported) "Seed Phrase" else "Seedless", ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt index 90e94e979a..ccb889e197 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isImported import com.tangem.domain.models.wallet.isLocked import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -32,11 +33,4 @@ internal class SelectedWalletAnalyticsSender @Inject constructor( else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported()) } - - private fun UserWallet.isImported(): Boolean { - return when (this) { - is UserWallet.Cold -> isImported - is UserWallet.Hot -> true - } - } } \ No newline at end of file From f10002c0e574cbfe9b707d151a14e7b5b31d4c69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:05:00 +0400 Subject: [PATCH 18/33] Updated on 2026-08-14 --- .../notifications/YieldSupplyNotificationsComponent.kt | 2 ++ .../notifications/model/YieldSupplyNotificationsModel.kt | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt index 0e5ade929b..533ecd122d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.notifications import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -35,6 +36,7 @@ internal class YieldSupplyNotificationsComponent( modifier = modifier .fillMaxWidth() .animateContentSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { state.forEachIndexed { index, item -> Notification( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 2e81b51d3f..58e72d7f9a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -91,7 +91,9 @@ internal class YieldSupplyNotificationsModel @Inject constructor( uiState.update { notifications.toPersistentList() } - yieldSupplyNotificationsUpdateListener.callbackHasError(notifications.any()) + // Business requirement that YieldSupplyHighNetworkFee is not an error and doesn't block the button + val hasError = notifications.any { it !is NotificationUM.Info.YieldSupplyHighNetworkFee } + yieldSupplyNotificationsUpdateListener.callbackHasError(hasError) }.launchIn(modelScope) } From c3c4eaf1f671c70c15c3c02588f8a75d1fbdd777 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 10:02:42 +0400 Subject: [PATCH 19/33] Updated on 2026-08-14 --- .../api/analytics/YieldSupplyAnalytics.kt | 11 +++++++ .../model/YieldSupplyNotificationsModel.kt | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index 37b00d61b2..4b7b61f898 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -204,6 +204,17 @@ sealed class YieldSupplyAnalytics( ), ) + data class NoticeHighFee( + val token: String, + val blockchain: String, + ) : YieldSupplyAnalytics( + event = "Notice - High Network Fee", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + enum class Action(val value: String) { Start("Start"), Approve("Approve"), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 58e72d7f9a..fc8a944729 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -80,14 +80,7 @@ internal class YieldSupplyNotificationsModel @Inject constructor( } } - if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { - analyticsEventHandler.send( - YieldSupplyAnalytics.NoticeNotEnoughFee( - token = cryptoCurrencyStatus.currency.symbol, - blockchain = cryptoCurrencyStatus.currency.network.name, - ), - ) - } + sendAnalytics(notifications, cryptoCurrencyStatus.currency) uiState.update { notifications.toPersistentList() } @@ -97,6 +90,26 @@ internal class YieldSupplyNotificationsModel @Inject constructor( }.launchIn(modelScope) } + private fun sendAnalytics(notifications: List, currency: CryptoCurrency) { + if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { + analyticsEventHandler.send( + YieldSupplyAnalytics.NoticeNotEnoughFee( + token = currency.symbol, + blockchain = currency.network.name, + ), + ) + } + + if (notifications.any { it is NotificationUM.Info.YieldSupplyHighNetworkFee }) { + analyticsEventHandler.send( + YieldSupplyAnalytics.NoticeHighFee( + token = currency.symbol, + blockchain = currency.network.name, + ), + ) + } + } + private fun openTokenDetails(cryptoCurrency: CryptoCurrency) { appRouter.push( AppRoute.CurrencyDetails( From 4cf3d71648c6938a91c81e9aa3aa0a5933302f89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 11:53:17 +0500 Subject: [PATCH 20/33] Updated on 2026-08-14 --- .../api/stakekit/models/response/model/NetworkTypeDTO.kt | 3 +++ .../local/token/converter/StakingNetworkTypeConverter.kt | 2 ++ .../kotlin/com/tangem/domain/models/staking/NetworkType.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt index 77bfadc65c..9cb0b84276 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt @@ -77,6 +77,9 @@ enum class NetworkTypeDTO { @Json(name = "canto") CANTO, + @Json(name = "cardano") + CARDANO, + @Json(name = "chihuahua") CHIHUAHUA, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt index adc411b5e5..4990e03bbf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt @@ -33,6 +33,7 @@ object StakingNetworkTypeConverter : TwoWayConverter NetworkType.BAND_PROTOCOL NetworkTypeDTO.BITSONG -> NetworkType.BITSONG NetworkTypeDTO.CANTO -> NetworkType.CANTO + NetworkTypeDTO.CARDANO -> NetworkType.CARDANO NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA NetworkTypeDTO.COMDEX -> NetworkType.COMDEX NetworkTypeDTO.COREUM -> NetworkType.COREUM @@ -106,6 +107,7 @@ object StakingNetworkTypeConverter : TwoWayConverter NetworkTypeDTO.BAND_PROTOCOL NetworkType.BITSONG -> NetworkTypeDTO.BITSONG NetworkType.CANTO -> NetworkTypeDTO.CANTO + NetworkType.CARDANO -> NetworkTypeDTO.CARDANO NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA NetworkType.COMDEX -> NetworkTypeDTO.COMDEX NetworkType.COREUM -> NetworkTypeDTO.COREUM diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt index 3e0921c351..e26a393e18 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt @@ -28,6 +28,7 @@ enum class NetworkType { BAND_PROTOCOL, BITSONG, CANTO, + CARDANO, CHIHUAHUA, COMDEX, COREUM, From 9127624b78ca5ec71bd3b842c7b85ccf739153a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 11:53:39 +0500 Subject: [PATCH 21/33] Updated on 2026-08-14 --- .../impl/presentation/model/StakingModel.kt | 17 ++++++++++++++--- .../state/utils/StakingPendingActionUtils.kt | 16 ++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9e1aaa62ba..70e39f6f9b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -93,6 +93,7 @@ import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -601,10 +602,20 @@ internal class StakingModel @Inject constructor( override fun onActiveStake(activeStake: BalanceState) { val networkId = cryptoCurrencyStatus.currency.network.rawId - if (isSingleAction(networkId, activeStake)) { + val preferredValidators = yield.validators.filter { it.preferred } + val pendingActions = activeStake.pendingActions.mapNotNull { action -> + if (action.type in listOf(StakingActionType.RESTAKE, StakingActionType.STAKE) && + preferredValidators.isSingleItem() + ) { + null + } else { + action + } + }.toImmutableList() + if (isSingleAction(networkId, pendingActions)) { prepareForConfirmation( balanceType = activeStake.type, - pendingActions = activeStake.pendingActions, + pendingActions = pendingActions, balanceState = activeStake, validator = activeStake.validator, amountValue = activeStake.cryptoValue, @@ -613,7 +624,7 @@ internal class StakingModel @Inject constructor( } else { stateController.update( ShowActionSelectorBottomSheetTransformer( - pendingActions = withStubUnstakeAction(networkId, activeStake), + pendingActions = withStubUnstakeAction(networkId, pendingActions, activeStake), onActionSelect = { action -> prepareForConfirmation( balanceType = activeStake.type, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index 6ef20b93a9..d229b97f60 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -37,17 +37,21 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t null -> TextReference.EMPTY } -internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean { - val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions - val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions) - val isRestake = activeStake.pendingActions.any { it.type.isRestake } +internal fun isSingleAction(networkId: String, pendingActions: List): Boolean { + val isSingleAction = pendingActions.size <= 1 // Either single or none pending actions + val isCompositePendingActions = isCompositePendingActions(networkId, pendingActions.toPersistentList()) + val isRestake = pendingActions.any { it.type.isRestake } return isSingleAction && !isRestake || isCompositePendingActions } -internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList { +internal fun withStubUnstakeAction( + networkId: String, + pendingActions: List, + activeStake: BalanceState, +): ImmutableList { return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) { - activeStake.pendingActions.plus( + pendingActions.plus( PendingAction( type = StakingActionType.UNSTAKE, passthrough = "", From a6598eccc8789bfade92ca906367b4aaa22d253c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 15:41:05 +0500 Subject: [PATCH 22/33] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 17 ++--- .../ui/tokens/TokenItemStateConverter.kt | 70 +++++++------------ .../tangem/utils/coroutines/CoroutineExt.kt | 21 ++++++ .../staking/usecase/StakingApyFlowUseCase.kt | 36 ---------- .../usecase/StakingAvailabilityListUseCase.kt | 37 ++++++++++ .../implementors/MultiWalletContentLoader.kt | 6 +- .../MultiWalletContentLoaderFactory.kt | 6 +- .../SingleWalletWithTokenContentLoader.kt | 6 +- ...ngleWalletWithTokenContentLoaderFactory.kt | 6 +- .../transformers/SetTokenListTransformer.kt | 7 +- .../converter/TokenListStateConverter.kt | 7 +- .../subscribers/AccountListSubscriber.kt | 33 +++++---- .../subscribers/BasicAccountListSubscriber.kt | 17 ++--- .../subscribers/BasicTokenListSubscriber.kt | 24 +++---- .../MultiWalletTokenListSubscriber.kt | 6 +- .../SingleWalletWithTokenListSubscriber.kt | 6 +- 16 files changed, 156 insertions(+), 149 deletions(-) delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 631f9a46a0..aea1890763 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,15 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* -import com.tangem.domain.staking.repositories.P2PEthPoolRepository -import com.tangem.domain.staking.repositories.StakeKitActionRepository -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakeKitRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository +import com.tangem.domain.staking.repositories.* import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import com.tangem.domain.staking.toggles.StakingFeatureToggles -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -238,10 +232,7 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideStakingApyFlowUseCase( - stakeKitRepository: StakeKitRepository, - stakingFeatureToggles: StakingFeatureToggles, - ): StakingApyFlowUseCase { - return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles) + fun provideStakingApyFlowUseCase(stakingRepository: StakingRepository): StakingAvailabilityListUseCase { + return StakingAvailabilityListUseCase(stakingRepository) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index ce46ff400f..029babd400 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -1,6 +1,5 @@ package com.tangem.common.ui.tokens -import com.tangem.blockchain.common.Blockchain import com.tangem.common.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -22,7 +21,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.model.isStakingSupported +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils @@ -44,7 +44,7 @@ import java.math.BigDecimal class TokenItemStateConverter( private val appCurrency: AppCurrency, private val yieldModuleApyMap: Map = emptyMap(), - private val stakingApyMap: Map> = emptyMap(), + private val stakingApyMap: Map = emptyMap(), private val yieldSupplyPromoBannerKey: String? = null, private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) @@ -177,7 +177,7 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, yieldModuleApyMap: Map, - stakingApyMap: Map>, + stakingApyMap: Map, onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?, ): TokenItemState.TitleState { return when (val value = currencyStatus.value) { @@ -217,7 +217,7 @@ class TokenItemStateConverter( private fun resolveEarnApy( cryptoCurrencyStatus: CryptoCurrencyStatus, yieldModuleApyMap: Map, - stakingApyMap: Map>, + stakingApyMap: Map, ): EarnApyInfo? { val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token if (token != null && yieldModuleApyMap.isNotEmpty()) { @@ -272,46 +272,42 @@ class TokenItemStateConverter( private fun findStakingRate( currencyStatus: CryptoCurrencyStatus, - stakingApyMap: Map>, + stakingApyMap: Map, ): StakingLocalInfo { - val stakingKey = currencyStatus.currency.stakingKey() - val validators = stakingApyMap[stakingKey] + val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) val yieldBalance = currencyStatus.value.yieldBalance val hasStakedBalance = yieldBalance is YieldBalance.Data - val rateInfo: Pair? = if (hasStakedBalance) { - val validatorsByAddress = validators.associateBy { it.address } - yieldBalance.balance.items - .mapNotNull { it.validatorAddress } - .firstNotNullOfOrNull { address -> - val validator = validatorsByAddress[address] - validator?.rewardInfo?.rate?.let { rate -> - rate to validator.rewardInfo?.type - } - } - ?: validators + val rateInfo = when (val stakingOptions = stakingAvailability.option) { + is StakingOption.P2P -> null // todo p2p + is StakingOption.StakeKit -> if (hasStakedBalance) { + val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } + yieldBalance.balance.items + .mapNotNull { it.validatorAddress } + .firstNotNullOfOrNull { address -> + validatorsByAddress[address]?.rewardInfo + } ?: stakingOptions.yield.validators .filter { it.preferred } .mapNotNull { validator -> - validator.rewardInfo?.rate?.let { rate -> rate to validator.rewardInfo?.type } + validator.rewardInfo } - .maxByOrNull { it.first } - } else { - validators - .filter { it.preferred } - .mapNotNull { validator -> - validator.rewardInfo?.rate?.let { rate -> - rate to validator.rewardInfo?.type + .maxByOrNull { it.rate } + } else { + stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo } - } - .maxByOrNull { it.first } + .maxByOrNull { it.rate } + } } return StakingLocalInfo( - rate = rateInfo?.first, + rate = rateInfo?.rate, isActive = hasStakedBalance, - rewardType = rateInfo?.second, + rewardType = rateInfo?.type, ) } @@ -450,18 +446,6 @@ class TokenItemStateConverter( } fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE - - private fun CryptoCurrency.stakingKey(): String { - if (this is CryptoCurrency.Coin && !network.isStakingSupported) return "" - - if (network.isStakingSupported && this !is CryptoCurrency.Coin) { - val isPolygonTokenOnEthereum = this is CryptoCurrency.Token && - this.network.id.rawId.value == Blockchain.Ethereum.id && - this.symbol == Blockchain.Polygon.currency - if (!isPolygonTokenOnEthereum) return "" - } - return "${id.rawCurrencyId}_$symbol" - } } private data class StakingLocalInfo( diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 7d14d15efb..1a52fae358 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -68,6 +68,27 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } } +@Suppress("LongParameterList", "MagicNumber") +inline fun combine6( + flow1: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + @Suppress("UNCHECKED_CAST") + transform( + arr[0] as T1, + arr[1] as T2, + arr[2] as T3, + arr[3] as T4, + arr[4] as T5, + arr[5] as T6, + ) +} + @Suppress("LongParameterList", "MagicNumber") inline fun combine7( flow1: Flow, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt deleted file mode 100644 index 22f83164b2..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.domain.staking.usecase - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.repositories.StakeKitRepository -import com.tangem.domain.staking.toggles.StakingFeatureToggles -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** - * Emits a map of Validators values per currency for staking. - * - * Return map: - * - key: currency staking key (network.backendId + "_" + symbol) - * - value: validators - */ -class StakingApyFlowUseCase( - private val stakeKitRepository: StakeKitRepository, - private val stakingFeatureToggles: StakingFeatureToggles, -) { - - operator fun invoke(): Flow>> { - return stakeKitRepository.getEnabledYields() - .map { yields -> - yields.filterNot { yield -> - val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId() - isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled - }.associate { yield -> - val key = "${yield.token.coinGeckoId}_${yield.token.symbol}" - val apy = yield.validators - key to apy - } - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt new file mode 100644 index 0000000000..e38ee7233c --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.staking.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** + * Returns staking availability for a list of crypto currencies for a specific user wallet + * + * Return map: + * - key: crypto currency + * - value: staking availability for the currency + */ +class StakingAvailabilityListUseCase( + private val stakingRepository: StakingRepository, +) { + + suspend fun invokeSync( + userWalletId: UserWalletId, + cryptoCurrencyList: List, + ): Map { + return coroutineScope { + cryptoCurrencyList.map { cryptoCurrency -> + async { + cryptoCurrency to stakingRepository.getStakingAvailabilitySync( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + } + }.awaitAll().toMap() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index e08e469afc..020e48bbb2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -45,7 +45,7 @@ internal class MultiWalletContentLoader( private val walletsRepository: WalletsRepository, private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, @@ -64,7 +64,7 @@ internal class MultiWalletContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, applyTokenListSortingUseCase = applyTokenListSortingUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 1f9d0570e8..083c1cdd5b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -44,7 +44,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, @@ -70,7 +70,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getNFTCollectionsUseCase = getNFTCollectionsUseCase, currenciesRepository = currenciesRepository, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 6d895697ed..b5c79b14b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -34,7 +34,7 @@ internal class SingleWalletWithTokenContentLoader( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -50,7 +50,7 @@ internal class SingleWalletWithTokenContentLoader( tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) MultiWalletWarningsSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 4d89203c74..bcf4c535a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -4,7 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -35,7 +35,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { @@ -55,7 +55,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 0dec24b4b9..39aca4fd2f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -19,7 +20,7 @@ internal class SetTokenListTransformer( private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, private val yieldSupplyApyMap: Map = emptyMap(), - private val stakingApyMap: Map> = emptyMap(), + private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, ) : WalletStateTransformer(userWallet.walletId) { @@ -64,7 +65,7 @@ internal class SetTokenListTransformer( appCurrency = appCurrency, clickIntents = clickIntents, yieldModuleApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 257e68fc6b..2520491085 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -14,11 +14,12 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState @@ -38,7 +39,7 @@ internal class TokenListStateConverter( private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, private val yieldModuleApyMap: Map, - private val stakingApyMap: Map>, + private val stakingAvailabilityMap: Map, private val shouldShowMainPromo: Boolean, ) : Converter { @@ -72,7 +73,7 @@ internal class TokenListStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params), - stakingApyMap = stakingApyMap, + stakingApyMap = stakingAvailabilityMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index d1cbb38064..5fb22bdc2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.utils.coroutines.combine7 +import com.tangem.utils.coroutines.combine6 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,29 +31,36 @@ internal class AccountListSubscriber @AssistedInject constructor( override val stateController: WalletStateController, override val clickIntents: WalletClickIntents, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), - flow6 = stakingApyFlow(), - flow7 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = ::updateState, - ) + flow6 = yieldSupplyGetShouldShowMainPromoFlow(), + ) { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo -> + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( + userWalletId = userWallet.walletId, + cryptoCurrencyList = accountList.flattenCurrencies().map(CryptoCurrencyStatus::currency), + ), + shouldShowMainPromo = shouldShowMainPromo, + ) + } private fun yieldSupplyApyFlow(): Flow> { return yieldSupplyApyFlowUseCase().distinctUntilChanged() } - private fun stakingApyFlow(): Flow>> { - return stakingApyFlowUseCase().distinctUntilChanged() - } - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow { return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 080766b9b3..fa02716faf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -8,8 +8,9 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies @@ -48,7 +49,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { expandedAccounts: Set, isAccountMode: Boolean, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean = false, ) { val accountFlattenCurrencies = accountList.flattenCurrencies() @@ -68,7 +69,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, portfolioId = PortfolioId(mainAccount.accountId), yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -89,7 +90,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params = convertParams, appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -102,7 +103,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency: AppCurrency, portfolioId: PortfolioId, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, ) { val tokenList = maybeTokenList.getOrElse( @@ -132,7 +133,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params = TokenConverterParams.Wallet(portfolioId, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -141,7 +142,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params: TokenConverterParams, appCurrency: AppCurrency, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, ) { stateController.update( @@ -151,7 +152,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ), ) 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 36de78ef22..3fbfb3592d 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 @@ -8,11 +8,12 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -41,7 +42,7 @@ internal abstract class BasicTokenListSubscriber( private val walletWithFundsChecker: WalletWithFundsChecker, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletSubscriber() { @@ -71,9 +72,8 @@ internal abstract class BasicTokenListSubscriber( }, flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), - flow4 = stakingApyFlow(), - flow5 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo -> + flow4 = yieldSupplyGetShouldShowMainPromoFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -101,7 +101,10 @@ internal abstract class BasicTokenListSubscriber( params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( + userWalletId = userWallet.walletId, + cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), + ), shouldShowMainPromo = shouldShowMainPromo, ) @@ -128,7 +131,7 @@ internal abstract class BasicTokenListSubscriber( params: TokenConverterParams, appCurrency: AppCurrency, yieldSupplyApyMap: Map, - stakingApyMap: Map>, + stakingAvailabilityMap: Map, shouldShowMainPromo: Boolean, ) { stateHolder.update( @@ -138,7 +141,7 @@ internal abstract class BasicTokenListSubscriber( appCurrency = appCurrency, clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ), ) @@ -156,9 +159,6 @@ internal abstract class BasicTokenListSubscriber( private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() - private fun stakingApyFlow(): Flow>> = stakingApyFlowUseCase() - .distinctUntilChanged() - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() .distinctUntilChanged() } \ No newline at end of file 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 d75d637dad..c3159ba551 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 @@ -8,7 +8,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase @@ -32,7 +32,7 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingApyFlowUseCase: StakingApyFlowUseCase, + stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, @@ -42,7 +42,7 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index 9e148dcf24..af21e47dc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -5,7 +5,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -27,7 +27,7 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingApyFlowUseCase: StakingApyFlowUseCase, + stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, @@ -37,7 +37,7 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { From 85a2344f628bb90245cd3764f8e009f2b19be9fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 12:12:52 +0400 Subject: [PATCH 23/33] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5b03c3345..6f915e67ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -275,8 +275,8 @@ dependencies { debugImplementation(projects.features.kyc.impl) internalImplementation(projects.features.kyc.impl) mockedImplementation(projects.features.kyc.impl) - releaseImplementation(projects.features.kyc.mock) - externalImplementation(projects.features.kyc.mock) + releaseImplementation(projects.features.kyc.impl) + externalImplementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) From 92523aa0dab87ec1348f7976db2ad3f6c0794845 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 14:53:01 +0400 Subject: [PATCH 24/33] Updated on 2026-08-14 --- .../tangem/common/ui/notifications/NotificationsFactory.kt | 7 +++++++ .../notifications/model/YieldSupplyNotificationsModel.kt | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 997da53ad1..668943b4c7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -108,6 +108,13 @@ object NotificationsFactory { } } + // Must be called last – shown only when no other notifications exist + fun MutableList.addHighFeeNotificationIfNoOther(shouldShowHighFeeNotification: Boolean) { + if (shouldShowHighFeeNotification && this.isEmpty()) { + add(NotificationUM.Info.YieldSupplyHighNetworkFee) + } + } + fun MutableList.addReserveAmountErrorNotification( reserveAmount: BigDecimal?, sendingAmount: BigDecimal, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index fc8a944729..9dc753015c 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -5,6 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addHighFeeNotificationIfNoOther import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -75,9 +76,9 @@ internal class YieldSupplyNotificationsModel @Inject constructor( onReload = params.callback::onFeeReload, ) - if (data.shouldShowHighFeeNotification) { - add(NotificationUM.Info.YieldSupplyHighNetworkFee) - } + addHighFeeNotificationIfNoOther( + shouldShowHighFeeNotification = data.shouldShowHighFeeNotification, + ) } sendAnalytics(notifications, cryptoCurrencyStatus.currency) From ec59339d51d91ed8a64fd9e233a0fe51a5993189 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 17:52:15 +0300 Subject: [PATCH 25/33] Updated on 2026-08-14 --- .../com/tangem/tap/features/welcome/model/WelcomeModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 9ca6d43223..9664e2bb7f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.welcome.model import com.tangem.common.core.TangemError import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -28,6 +28,7 @@ import javax.inject.Inject internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val appFinisher: AppFinisher, + private val analyticsEventsHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model(), StoreSubscriber { @@ -44,6 +45,7 @@ internal class WelcomeModel @Inject constructor( init { subscribeToStoreChanges() initGlobalState() + analyticsEventsHandler.send(SignIn.ScreenOpened()) val welcomeAction = when (params.launchMode) { is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard @@ -54,12 +56,12 @@ internal class WelcomeModel @Inject constructor( } private fun unlockWallets() { - Analytics.send(SignIn.ButtonBiometricSignIn()) + analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn()) store.dispatch(WelcomeAction.ProceedWithBiometrics) } private fun scanCard() { - Analytics.send(SignIn.ButtonCardSignIn()) + analyticsEventsHandler.send(SignIn.ButtonCardSignIn()) store.dispatch(WelcomeAction.ProceedWithCard) } From 9be5646b7826aa45af346abe26b6d7bd054c0b0d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 20:02:54 +0500 Subject: [PATCH 26/33] Updated on 2026-08-14 --- .../pay/models/response/BalanceResponse.kt | 6 +-- .../repository/DefaultOnboardingRepository.kt | 8 ++-- .../DefaultTangemPayCardDetailsRepository.kt | 44 ++++++++++++------- .../tangempay/entity/TangemPayDetailsUM.kt | 1 - .../transformers/DetailsBalanceTransformer.kt | 8 ---- .../tangempay/ui/TangemPayDetailsScreen.kt | 37 ---------------- 6 files changed, 36 insertions(+), 68 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt index 935c7f23e2..caccad1737 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt @@ -6,9 +6,9 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class BalanceResponse( - @Json(name = "fiat") val fiat: FiatBalance, - @Json(name = "crypto") val crypto: CryptoBalance, - @Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal, + @Json(name = "fiat") val fiat: FiatBalance?, + @Json(name = "crypto") val crypto: CryptoBalance?, + @Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal?, ) @JsonClass(generateAdapter = true) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 563b3ee0eb..f3a791fb06 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -139,13 +139,13 @@ internal class DefaultOnboardingRepository @Inject constructor( response: CustomerMeResponse.Result?, ): CustomerInfo { val card = response?.card - val balance = response?.balance + val fiatBalance = response?.balance?.fiat val paymentAccount = response?.paymentAccount - val cardInfo = if (paymentAccount != null && card != null && balance != null) { + val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { CardInfo( lastFourDigits = card.cardNumberEnd, - balance = balance.fiat.availableBalance, - currencyCode = balance.fiat.currency, + balance = fiatBalance.availableBalance, + currencyCode = fiatBalance.currency, customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 1dafcfd572..b39bff96f8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -52,20 +52,29 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( private val storePollingMutex = Mutex() override suspend fun getCardBalance(userWalletId: UserWalletId): Either { - return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request(userWalletId) { authHeader -> - tangemPayApi.getCardBalance(authHeader) - }.result ?: error("Cannot get card balance") - TangemPayCardBalance( - fiatBalance = result.fiat.availableBalance, - currencyCode = result.fiat.currency, - cryptoBalance = result.crypto.balance, - availableForWithdrawal = result.availableForWithdrawal.amount, - chainId = result.crypto.chainId, - depositAddress = result.crypto.depositAddress, - contractAddress = result.crypto.tokenContractAddress, - ) - } + return catch( + block = { + val response = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getCardBalance(authHeader) + }.getOrNull() + + val fiatBalance = requireNotNull(response?.result?.fiat) { "Cannot get card balance fiat" } + val cryptoBalance = requireNotNull(response.result?.crypto) { "Cannot get card balance crypto" } + val withdrawalAmount = requireNotNull(response.result?.availableForWithdrawal) { + "Cannot get card balance availableForWithdrawal" + } + TangemPayCardBalance( + fiatBalance = fiatBalance.availableBalance, + currencyCode = fiatBalance.currency, + cryptoBalance = cryptoBalance.balance, + availableForWithdrawal = withdrawalAmount.amount, + chainId = cryptoBalance.chainId, + depositAddress = cryptoBalance.depositAddress, + contractAddress = cryptoBalance.tokenContractAddress, + ).right() + }, + catch = ::catchException, + ) } override suspend fun revealCardDetails(userWalletId: UserWalletId): Either { @@ -100,7 +109,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( expirationMonth = result.expirationMonth, ).right() }, - catch = { errorConverter.convert(it).left() }, + catch = ::catchException, ) } @@ -285,6 +294,11 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } + private fun catchException(throwable: Throwable): Either { + Timber.tag(TAG).e(throwable) + return errorConverter.convert(throwable).left() + } + private companion object { const val MAX_POLLING_RETRIES = 3 } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index a8b31086f1..37663aa1f0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -41,7 +41,6 @@ internal sealed class TangemPayDetailsBalanceBlockState { data class Content( override val actionButtons: ImmutableList, - val cryptoBalance: String, val fiatBalance: String, val isBalanceFlickering: Boolean, ) : TangemPayDetailsBalanceBlockState() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt index 857920d456..d2da85c3d4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt @@ -2,10 +2,8 @@ package com.tangem.features.tangempay.model.transformers import arrow.core.Either import com.tangem.core.error.UniversalError -import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance @@ -13,7 +11,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal import java.util.Currency internal class DetailsBalanceTransformer( @@ -37,7 +34,6 @@ internal class DetailsBalanceTransformer( TangemPayDetailsBalanceBlockState.Content( isBalanceFlickering = false, fiatBalance = getFiatBalanceText(balance.value), - cryptoBalance = getCryptoBalanceText(balance.value.cryptoBalance, cryptoCurrency), actionButtons = prevState.balanceBlockState.actionButtons, ) } @@ -52,8 +48,4 @@ internal class DetailsBalanceTransformer( fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) } } - - private fun getCryptoBalanceText(cryptoBalance: BigDecimal, cryptoCurrency: CryptoCurrency): String { - return cryptoBalance.format { crypto(cryptoCurrency = cryptoCurrency) } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index 6addbcf3d6..255f5c37a5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -175,11 +175,6 @@ private fun TangemPayDetailsBalanceBlock( state = state, isBalanceHidden = isBalanceHidden, ) - CryptoBalance( - modifier = Modifier.padding(start = 12.dp, top = 4.dp), - state = state, - isBalanceHidden = isBalanceHidden, - ) if (state.actionButtons.isNotEmpty()) { HorizontalActionChips( modifier = Modifier.padding(top = 12.dp), @@ -220,37 +215,6 @@ private fun FiatBalance( ) } } - -@Suppress("UnusedPrivateMember") -@Composable -private fun CryptoBalance( - state: TangemPayDetailsBalanceBlockState, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - when (state) { - is TangemPayDetailsBalanceBlockState.Loading -> RectangleShimmer( - modifier = modifier.size( - width = TangemTheme.dimens.size70, - height = TangemTheme.dimens.size16, - ), - ) - is TangemPayDetailsBalanceBlockState.Content -> Text( - modifier = modifier, - text = state.cryptoBalance.orMaskWithStars(isBalanceHidden), - style = TangemTheme.typography.caption2.applyBladeBrush( - isEnabled = state.isBalanceFlickering, - textColor = TangemTheme.colors.text.tertiary, - ), - ) - is TangemPayDetailsBalanceBlockState.Error -> Text( - modifier = modifier, - text = DASH_SIGN.orMaskWithStars(isBalanceHidden), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} // endregion @OptIn(ExperimentalMaterial3Api::class) @@ -345,7 +309,6 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider Date: Fri, 19 Dec 2025 11:53:17 +0400 Subject: [PATCH 27/33] Updated on 2026-08-14 --- core/res/src/main/res/values-fr/strings.xml | 2 + core/res/src/main/res/values-ja/strings.xml | 32 +++-- core/res/src/main/res/values-ru/strings.xml | 9 +- core/res/src/main/res/values/strings.xml | 36 ++++-- .../DefaultWalletManagersFacade.kt | 6 +- .../SdkTransactionHistoryItemConverter.kt | 8 +- .../utils/SdkTransactionTypeConverter.kt | 44 +++++-- .../utils/YieldSupplyAddresses.kt | 113 ++++++++++++++++++ .../tangem/domain/models/network/TxInfo.kt | 7 +- ...xHistoryItemToTransactionStateConverter.kt | 27 ++--- .../converter/TxHistoryItemStateConverter.kt | 16 +-- 11 files changed, 240 insertions(+), 60 deletions(-) create mode 100644 data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index faddf299c6..dea8c07645 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -896,6 +896,8 @@ La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée et le supprimera de l\'application. Vous ne pourrez pas restaurer le portefeuille actuel. Les propriétaires du Tangem Ring ont droit à 3 swap gratis sur Changelly jusqu\'au 15/11 ! Échangez avec 0 % de frais ! + Les appareils disposant d\'un accès root sont considérés comme moins sécurisés. Vos données peuvent être exposées à des risques supplémentaires. + Accès root détecté Connectez-vous à l\'application et vérifiez votre solde sans scanner la carte Accéder à l\'application Autoriser l\'utilisation de la biométrie diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index bce005b585..70f1b95349 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -103,10 +103,10 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 - 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 - これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 + 安全に保管するため、後ほどアクセスコードの入力を求められます。 + 保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 - これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 + 保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引に署名するたびにアクセスコードを入力する必要があります。 アクセスコードを保存 @@ -144,10 +144,10 @@ 残高は非表示 ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに! ベータモード - デバイスで生体認証がオフになっているため、ウォレットのロック解除に使用できません。生体認証を再度利用するには、デバイスの設定で有効にしてください。 + この端末では生体認証がオフになっているため、ウォレットの解除に使用できません。再度この方法を使用するには、端末の設定で生体認証を有効にしてください。 生体認証が無効になっています カードまたはリングをスキャンしてください - 生体認証の試行回数が上限に達しました。端末タップまたはアクセスコードでウォレットを解除してください。 + 生体認証の試行回数の上限に達しました。カード/リングでウォレットを解除するか、アクセスコードを入力してください。 生体認証がロックされています 30秒後に再試行するか、カードまたはリングをスキャンしてください 生体認証ログインは一時的にロックされています。30秒後にもう一度お試しいただくか、端末タップまたはアクセスコードでウォレットを解除してください。 @@ -179,7 +179,7 @@ もう一度アップグレードしてください リセット完了 このウォレット内のすべてのTangemデバイスで、リセット処理を完了することを推奨します。 - すべてのTangemデバイスをリセットしていません + リセットが必要なTangemデバイスがあります。 このカードを使用して、このウォレット内の他のカードまたはリングのアクセスコードをリセットしたくない場合は、このオプションを無効にしてください。これにより、このカードのアクセスコードもリセットできなくなりますのでご注意ください。 このカードを使用して、このウォレット内の他のカードのアクセスコードをリセットできます。 アクセスコードの復元 @@ -378,6 +378,7 @@ 送金 データを読み込めません… わかりました + 理解して続行 エラーが発生しました。もう一度お試しください。 アクセスできません ステーキング解除 @@ -567,7 +568,7 @@ 新しいウォレットを作成する Tangemを注文 Tangemをスキャン - 「Tangem」に生体認証の使用を許可しますか?\n本人確認とアプリの起動のために使用されます。 + 「Tangem」が生体認証を使用して本人確認を行い、アプリを開くことを許可しますか? %sへ %sネットワーク アクセスコードの設定をキャンセルしてもよろしいですか? @@ -1059,6 +1060,7 @@ カードをリセットする この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。 このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。 + 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました 工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。 工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。 すべてのTangemデバイスがリセットされました。 @@ -1067,6 +1069,8 @@ 続行するには次のデバイスをリセットしてください Tangem Ringユーザーは、11/15日までChangelly経由でスワップを3回手数料ゼロで行えます! 今すぐ手数料0% でスワップしましょう! + Rootアクセスが有効な端末は、セキュリティが低いと判断されます。データが追加のリスクにさらされる可能性があります。 + Rootアクセスが検出されました アプリにログインして、カードまたはリングをスキャンせずに残高を確認できます アプリにアクセスする 生体認証の使用を許可する @@ -1402,6 +1406,7 @@ カードが凍結されています サポートを受ける その他 + Root化された端末では使用できません 完了 拒否 保留中 @@ -1414,6 +1419,8 @@ カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。 カードの凍結が解除されました 出金 + Root化された端末では使用できません + KYCをキャンセル 資金を追加 入金オプション カード番号 @@ -1441,6 +1448,7 @@ 準備完了!カードはすぐに使用できます。 Google Payにカードを追加する Apple Payにカードを追加する + PINコード アドレスを共有するか、QRコードを表示してください。 技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。 現在、受け取りは利用できません @@ -1449,12 +1457,15 @@ ポートフォリオ内のあらゆる資産をカードと交換 カードの詳細 カードの一時停止を解除 + 忘れた場合は、アプリに戻ってください。 + PINコード 出金 現在、出金ができません 現在の処理が完了するまでは、スワップや新しい出金を開始することはできません。 出金処理中です PINコードを変更 忘れた場合はアプリに戻って確認できます。 + 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました カードの発行に失敗しました 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 技術的なエラーが発生しました。サポートへお問い合わせください。 @@ -1471,6 +1482,7 @@ KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 + 以下のボタンから、現在のKYCステータスを確認するか、KYCをキャンセルできます。 暗号資産を、リアルな支払いに。\n他とはまったく違う、新しいタイプの決済カード。 Tangem Visaカード カードをGET @@ -1488,7 +1500,7 @@ 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 同期が必要です Tangem Visaカード - Tangem Payは一時的に利用できません + Tangem Payは現在一時的に利用できません。 Tangem Pay カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 PINコード @@ -1572,7 +1584,7 @@ プッシュ通知を使用しますか? プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。 取引を見逃さない - 新しいウォレットを追加 + ウォレットを追加 バックアップせずにこのウォレットを削除すると、資金に永久にアクセスできなくなります。 このウォレットを忘れてもよろしいですか? エラーが発生しました。カードまたはリングをスキャンしてログインしてください。 @@ -1879,7 +1891,7 @@ 不審な取引 すでにTangemウォレットをお持ちですか? 数千種類の資産 - 業界最高水準のハードウェアウォレット + 最高水準のハードウェアウォレット 迅速な配送 ワンタップで開始 シームレスで安全 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d5eaa197f..036b504b45 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -394,6 +394,7 @@ Перевод Невозможно загрузить данные… Я понял + Я понимаю, продолжить Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно Завершить стейкинг @@ -733,6 +734,7 @@ Лидеры роста Лидеры падения В тренде + Режим доходности Стейкинг — простой способ получать доход с вашей криптовалюты. %s Получайте до %s APY Токен добавлен @@ -994,6 +996,7 @@ Другие валюты Популярные фиаты Поиск по валюте + Эта транзакция уже была обработана. Дополнительных действий не требуется. Получение лучших курсов... Моментально Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s @@ -1104,6 +1107,8 @@ Сбросьте следующее устройство для продолжения. Владельцам колец — 3 обмена без комиссии на Changelly до 15.11! Обмен с 0% комиссией! + Устройства с root-доступом считаются менее безопасными. Ваши данные могут быть подвержены дополнительным рискам. + Обнаружен root-доступ Войдите в приложение и следите за своим балансом без сканирования карты или кольца Доступ в приложение Использовать биометрию @@ -1493,6 +1498,7 @@ Вывод выполняется Изменить PIN-код Можно посмотреть здесь, если забудете его. + Я понимаю, что полностью потеряю доступ к своей карте Tangem Pay и ко всем средствам на ней без возможности восстановления. Не удалось выпустить карту Техническая ошибка, попробуйте ещё раз, нажав кнопку ниже Техническая ошибка, свяжитесь с поддержкой @@ -1964,6 +1970,7 @@ Политика комиссий за пополнение Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода. Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. + Комиссии сейчас выше обычного из-за высокой активности на рынке. Вы можете продолжить сейчас или вернуться позже, когда комиссии снизятся. Высокая сетевая комиссия Историческая доходность Получай до %1$s APY на свой баланс @@ -2017,7 +2024,7 @@ %1$s выведено из Aave Режим доходности инициализирован Режим доходности реактивирован - Перевод средств в Aave + Перевод в Aave %1$s отправлено в Aave Вывод из Aave Автоматически diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d365c6e247..eaddf5b46e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -103,10 +103,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. - You’ll be asked for your wallet’s access code later so we can securely store it for future use - This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + You\'ll be asked for your access code later for secure storage. + This will delete all saved wallet access codes. You\'ll need to enter the access code again to use the wallet. Removing the saved devices deletes all the saved wallets and their access codes from the app. - This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. + This will delete all saved wallet access codes. You’ll need to enter the access code again to use the wallet. Require Access Code This option turns off biometrics for sensitive actions. You’ll need to enter your access code each time you sign a transaction. Save Access Code @@ -145,10 +145,10 @@ Balances are hidden According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates! Beta Mode - Biometrics are turned off on your device, so you can’t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. + Biometrics are turned off on your device, so you can\'t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. Biometric authentication disabled Please scan the card or ring - You’ve reached the limit of biometric attempts. Please unlock your wallet with a device tap or enter your access code. + You\'ve reached the limit of biometric attempts. Please unlock your wallet with a card/ring or enter your access code. Biometric authentication locked Please try again in 30 seconds or scan the card or ring Biometric login is temporarily locked. Please try again in 30 seconds, or unlock your wallet with a device tap or access code. @@ -182,7 +182,7 @@ Upgrade again Reset complete We recommend completing the reset process for all Tangem devices in this wallet. - You haven’t reset all your Tangem devices + Some Tangem devices still need to be reset. Disable this option if you don\'t want this card to be used to reset access codes on other cards or rings in this wallet. Please note that this will also prevent you from resetting the access code on this card. Allows you to use this card to reset access code on other cards in this wallet Access code recovery @@ -384,8 +384,9 @@ Transaction status Transactions Transfer - Unable to load the data… + Unable to load data… I understand + I understand, continue There was an error. Please try again. Unreachable Unstake @@ -575,7 +576,7 @@ Create New Wallet Order Tangem Scan Tangem - Do you want to allow “Tangem” to use biometric authentication? To confirm your identity and open the app + Do you want to allow \"Tangem\" to use biometric authentication to confirm your identity and open the app? to %s On %s network Are you sure you want to cancel access code setup? @@ -799,7 +800,7 @@ Add tokens Power up your assets while supplying them with instant access. %s Activate Yield Mode - You must update to %1$s in order to create mobile wallet + You must update to %1$s before creating a mobile wallet Mobile Wallet requires %1$s or later All news @@ -1082,6 +1083,7 @@ Reset the card I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards of the current wallet + I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code. Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet. All Tangem devices have been reset. @@ -1090,6 +1092,8 @@ Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! + Devices with root access are considered less secure. Your data may be exposed to additional risks. + Root access detected Log into the app and check your balance without scanning the card or ring Access the app Allow to use biometrics @@ -1426,6 +1430,7 @@ Your card is frozen. Get Help Other + Unable to use on rooted devices Completed Declined Pending @@ -1439,6 +1444,7 @@ Your card is unfrozen. Withdrawal Unable to use on rooted device + Cancel KYC Add funds Top-up options Card Number @@ -1466,6 +1472,7 @@ All set! Your card is ready to use. Add card to Google Pay Add card to Apple Pay + PIN code Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now @@ -1474,6 +1481,8 @@ Swap any asset in your portfolio for card Card details Unfreeze Card + Come back to the app if you forget it. + Your PIN code Withdraw Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished @@ -1497,6 +1506,7 @@ KYC in progress View Status KYC in progress for Tangem Pay + Use the buttons below to view your current KYC status or cancel it. Use your crypto for real world spending. \nIt’s a payment card unlike any other. Tangem Visa Card Get card @@ -1598,7 +1608,7 @@ Would you like to use\nPush-notifications? Enable push notifications to receive alerts when funds arrive in your wallet. Don\'t Miss a Transaction - Add new wallet + Add Wallet If you delete this wallet without a backup, you will permanently lose access to your funds. Are you sure you want to forget this wallet? An error has occurred, please scan your card or ring to log in @@ -1759,7 +1769,7 @@ Change access code Stay notified on wallet incoming transactions and Tangem updates. Push notifications may currently not work on Huawei devices. We\'re actively working on a solution and will release a fix in an upcoming update. Thank you for your understanding! - Transaction Notifications + Transaction notifications Set access code Wallet settings Tangem @@ -1952,7 +1962,7 @@ Suspicious transaction Already have Tangem Wallet? Thousands of assets - Best in class hardware wallet + Top-tier hardware wallet Fast delivery Start in one tap Seamless and secure @@ -1961,7 +1971,7 @@ Create or import a software wallet Create or import a software wallet on your phone. Start with Mobile Wallet - Other method + Other methods Use a Tangem hardware wallet Learn more & buy Discard diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 5b0f50b11d..00caf4f9d7 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -276,8 +276,10 @@ internal class DefaultWalletManagersFacade @Inject constructor( is Result.Success -> PaginationWrapper( currentPage = sdkPageConverter.convert(page), nextPage = sdkPageConverter.convert(itemsResult.data.nextPage), - items = SdkTransactionHistoryItemConverter(smartContractMethods = readSmartContractMethods()) - .convertList(itemsResult.data.items), + items = SdkTransactionHistoryItemConverter( + smartContractMethods = readSmartContractMethods(), + yieldSupplyAddresses = YIELD_SUPPLY_ADDRESSES, + ).convertList(itemsResult.data.items), ) is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 1a677b9cdb..51d7e2fd6e 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -8,9 +8,13 @@ import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem as internal class SdkTransactionHistoryItemConverter( smartContractMethods: Map, + yieldSupplyAddresses: Set, ) : Converter { - private val typeConverter by lazy { SdkTransactionTypeConverter(smartContractMethods) } + private val typeConverter by lazy { SdkTransactionTypeConverter( + smartContractMethods = smartContractMethods, + yieldSupplyAddresses = yieldSupplyAddresses, + ) } override fun convert(value: SdkTransactionHistoryItem): TxInfo = TxInfo( txHash = value.txHash, @@ -24,7 +28,7 @@ internal class SdkTransactionHistoryItemConverter( SdkTransactionHistoryItem.TransactionStatus.Failed -> TxInfo.TransactionStatus.Failed SdkTransactionHistoryItem.TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed }, - type = typeConverter.convert(value.type to value.destinationType.toDomain()), + type = typeConverter.convert(value), amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" }, ) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt index 5e9fe1dc0b..639350d503 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -1,5 +1,6 @@ package com.tangem.data.walletmanager.utils +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData @@ -11,17 +12,29 @@ import com.tangem.utils.converter.Converter internal class SdkTransactionTypeConverter( private val smartContractMethods: Map, -) : Converter, TxInfo.TransactionType> { + private val yieldSupplyAddresses: Set, +) : Converter { - override fun convert(value: Pair): TxInfo.TransactionType { - val (type, destination) = value + override fun convert(value: TransactionHistoryItem): TxInfo.TransactionType { + val (type, destination) = value.type to value.destinationType + val source = value.sourceType return when (type) { is TransactionType.ContractMethod -> { - getTransactionType(methodName = smartContractMethods[type.id]?.name, type.callData, destination) + getTransactionType( + methodName = smartContractMethods[type.id]?.name, + callData = type.callData, + destination = destination, + source = source, + ) } is TransactionType.ContractMethodName -> { - getTransactionType(methodName = type.name, type.callData, destination) + getTransactionType( + methodName = type.name, + callData = type.callData, + destination = destination, + source = source, + ) } is TransactionType.Transfer -> { TxInfo.TransactionType.Transfer @@ -48,7 +61,8 @@ internal class SdkTransactionTypeConverter( private fun getTransactionType( methodName: String?, callData: String?, - destination: TxInfo.DestinationType, + destination: TransactionHistoryItem.DestinationType, + source: TransactionHistoryItem.SourceType, ): TxInfo.TransactionType { return when (methodName) { "transfer" -> TxInfo.TransactionType.Transfer @@ -70,7 +84,21 @@ internal class SdkTransactionTypeConverter( "withdrawRewardsPOL", -> TxInfo.TransactionType.Staking.ClaimRewards "redelegate" -> TxInfo.TransactionType.Staking.Restake - "yieldSend" -> TxInfo.TransactionType.YieldSupply.Send + "yieldSend" -> { + val sourceAddresses = when (source) { + is TransactionHistoryItem.SourceType.Multiple -> source.addresses + is TransactionHistoryItem.SourceType.Single -> listOf(source.address) + }.map { + it.lowercase() + }.toSet() + + val isYieldSupplyWithdraw = + yieldSupplyAddresses.intersect(sourceAddresses).isNotEmpty() + + TxInfo.TransactionType.YieldSupply.Send( + isYieldSupplyWithdraw = isYieldSupplyWithdraw, + ) + } "enterProtocolByOwner" -> callData?.let { data -> TxInfo.TransactionType.YieldSupply.Enter( EthereumYieldSupplyEnterCallData.decode(data)?.tokenContractAddress.orEmpty(), @@ -82,7 +110,7 @@ internal class SdkTransactionTypeConverter( ) } "deployYieldModule" -> TxInfo.TransactionType.YieldSupply.DeployContract( - (destination as? TxInfo.DestinationType.Single)?.addressType?.address.orEmpty(), + (destination as? TransactionHistoryItem.DestinationType.Single)?.addressType?.address.orEmpty(), ) "initYieldToken" -> callData?.let { data -> TxInfo.TransactionType.YieldSupply.InitializeToken( diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt new file mode 100644 index 0000000000..22e28e3eb6 --- /dev/null +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt @@ -0,0 +1,113 @@ +package com.tangem.data.walletmanager.utils + +// Remove after moving this data to BE +internal val YIELD_SUPPLY_ADDRESSES: Set = setOf( + "0x00901a076785e0906d1028c7d6372d247bec7d61", + "0x00907f9921424583e7ffbfedf84f92b7b2be4977", + "0x018008bfb33d285247a21d44e50697654f754e63", + "0x067ae75628177fd257c2b1e500993e1a0babcbd1", + "0x078f358208685046a11c85e8ad32895ded33a249", + "0x0a1d576f3efef75b330424287a95a366e8281d54", + "0x0b925ed163218f6662a35e0f0371ac234f9e9371", + "0x0c0d01abf3e6adfca0989ebba9d6e85dd58eab1e", + "0x10ac93971cdb1f5c778144084242374473c350da", + "0x191c10aa4af7c30e871e70c95db0e4eb77237530", + "0x1ba9843bd4327c6c77011406de5fa8749f7e3479", + "0x1c0e06a0b1a4c160c17545ff2a951bfca57c0002", + "0x23878914efe38d27c4d67ab83ed1b93a74d4086a", + "0x24ab03a9a5bc2c49e5523e8d915a3536ac38b91d", + "0x2516e7b3f76294e03c42aa4c5b5b4dce9c436fb8", + "0x285866acb0d60105b4ed350a463361c2d9afa0e2", + "0x2d62109243b87c4ba3ee7ba1d91b0dd0a074d7b1", + "0x2e94171493fabe316b6205f1585779c887771e2f", + "0x2edff5af94334fbd7c38ae318edf1c40e072b73b", + "0x312ffc57778cefa11989733e6e08143e7e229c1c", + "0x32a6268f9ba3642dda7892add74f1d34469a4259", + "0x38a5357ce55c81add62abc84fb32981e2626adef", + "0x38c503a438185cde29b5cf4dc1442fd6f074f1cc", + "0x38d693ce1df5aadf7bc62595a37d667ad57922e5", + "0x3fe6a295459fae07df8a0cecc36f37160fe86aa9", + "0x40b4baecc69b882e8804f9286b12228c27f8c9bf", + "0x4199cc1f5ed0d796563d7ccb2e036253e2c18281", + "0x44705f578135cc5d703b4c9c122528c73eb87145", + "0x4579a27af00a62c0eb156349f31b345c08386419", + "0x481a2acf3a72ffdc602a9541896ca1db87f86cf7", + "0x4b0821e768ed9039a70ed1e80e15e76a5be5df5f", + "0x4c612e3b15b96ff9a6faed838f8d07d479a8dd4c", + "0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8", + "0x4e2a4d9b3df7aae73b418bd39f3af9e148e3f479", + "0x4e65fe4dba92790696d040ac24aa414708f5c0ab", + "0x4f5923fc5fd4a93352581b38b7cd26943012decf", + "0x513c7e3a9c69ca3e22550ef58ac1c0088e918fff", + "0x545bd6c032efdde65a377a6719def2796c8e0f2e", + "0x56a7ddc4e848ebf43845854205ad71d5d5f72d3d", + "0x5b502e3796385e1e9755d7043b9c945c3accec9c", + "0x5c647ce0ae10658ec44fa4e11a51c96e94efd1dd", + "0x5e8c8a7243651db1384c0ddfdbe39761e8e7e51a", + "0x5ee5bf7ae06d1be5997a1a72006fe6c607ec6de8", + "0x5f4a0873a3a02f7c0cb0e13a1d4362a1ad90e751", + "0x5f9190496e0dfc831c3bd307978de4a245e2f5cd", + "0x5fefd7069a7d91d01f269dade14526ccf3487810", + "0x625e7708f30ca75bfd92586e17077590c60eb4cd", + "0x62fc96b27a510cf4977b59ff952dc32378cc221d", + "0x6533afac2e7bccb20dca161449a13a32d391fb00", + "0x65906988adee75306021c417a1a3458040239602", + "0x67eaf2bee4384a2f84da9eb8105c661c123736ba", + "0x6ab707aca953edaefbc4fd23ba73294241490620", + "0x6b030ff3fb9956b1b69f475b77ae0d3cf2cc5afa", + "0x6d80113e533a2c0fe82eabd35f1875dcea89ea97", + "0x71aef7b30728b9bb371578f36c5a1f1502a5723e", + "0x724dc807b04555b71ed48a6896b6f41593b8c637", + "0x75bd1a659bdc62e4c313950d44a2416fab43e785", + "0x7b95ec873268a6bfc6427e7a28e396db9d0ebc65", + "0x7c307e128efa31f540f2e2d976c995e0b65f51f6", + "0x80a94c36747cf51b2fbabdff045f6d22c1930ed1", + "0x80ca0d8c38d2e2bcbab66aa1648bd1c7160500fe", + "0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee", + "0x82f9c5ad306bba1ad0de49bb5fa6f01bf61085ef", + "0x8437d7c167dfb82ed4cb79cd44b7a32a1dd95c77", + "0x8a2b6f94ff3a89a03e8c02ee92b55af90c9454a2", + "0x8a458a9dc9048e005d22849f470891b840296619", + "0x8a9fde6925a839f6b1932d16b36ac026f8d3fbdb", + "0x8eb270e296023e9d92081fdf967ddd7878724424", + "0x8ffdf2de812095b1d19cb146e4c004587c0a0692", + "0x90072a4aa69b5eb74984ab823efc5f91e90b3a72", + "0x90da57e0a6c0d166bf15764e03b83745dc90025b", + "0x927709711794f3de5ddbf1d176bee2d55ba13c21", + "0x977b6fc5de62598b08c85ac8cf2b745874e8b78c", + "0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c", + "0x99cbc45ea5bb7ef3a5bc08fb1b7e56bb2442ef0d", + "0x9a44fd41566876a39655f74971a3a6ea0a17a454", + "0x9b00a09492a626678e5a3009982191586c444df9", + "0xa4d94019934d8333ef880abffbf2fdd611c762bd", + "0xa700b4eb416be35b2911fd5dee80678ff64ff6c9", + "0xa9251ca9de909cb71783723713b21e4233fbf1b1", + "0xaa0200d169ff3ba9385c12e073c5d1d30434ae7b", + "0xaa6e91c82942aeae040303bf96c15a6dbcb82ca0", + "0xb76cf92076adbf1d9c39294fa8e7a67579fde357", + "0xb82fa9f31612989525992fcfbb09ab22eff5c85a", + "0xbcffb4b3beadc989bd1458740952af6ec8fbe431", + "0xbdb9300b7cde636d9cd4aff00f6f009ffbbc8ee6", + "0xbdfa7b7893081b35fb54027489e2bc7a38275129", + "0xbdfd4e51d3c14a232135f04988a42576efb31519", + "0xbe54767735fb7acca2aa7e2d209a6f705073536d", + "0xc45a479877e1e9dfe9fcd4056c699575a1045daa", + "0xc7b4c17861357b8abb91f25581e7263e08dcb59c", + "0xcc9ee9483f662091a1de4795249e24ac0ac2630f", + "0xcca43cef272c30415866914351fdfc3e881bb7c2", + "0xcf3d55c10db69f28fd1a75bd73f3d8a2d9c595ad", + "0xd4a0e0b9149bcee3c920d2e00b5de09138fd8bb7", + "0xd4e245848d6e1220dbe62e155d89fa327e43cb06", + "0xdd5745756c2de109183c6b5bb886f9207bef114d", + "0xde6ef6cb4abd3a473ffc2942eef5d84536f8e864", + "0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8", + "0xe728577e9a1fe7032bc309b4541f58f45443866e", + "0xea1132120ddcdda2f119e99fa7a27a0d036f7ac9", + "0xebe517846d0f36eced99c735cbf6131e1feb775d", + "0xec4ef66d4fceeba34abb4de69db391bc5476ccc8", + "0xf329e36c7bf6e5e86ce2150875a84ce77f477375", + "0xf59036caebea7dc4b86638dfa2e3c97da9fccd40", + "0xf611aeb5013fd2c0511c9cd55c7dc5c1140741a6", + "0xf6d2224916ddfbbab6e6bd0d1b7034f4ae0cab18", + "0xfa82580c16a31d0c1bc632a36f82e83efef3eec0", +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index d8fd074625..aae35a95f1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -124,9 +124,10 @@ data class TxInfo( } @Serializable - data object Send : YieldSupply { - override val address: String? = null - } + data class Send( + override val address: String? = null, + val isYieldSupplyWithdraw: Boolean, + ) : YieldSupply @Serializable data class DeployContract(override val address: String) : YieldSupply diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt index bc5f160b47..21045da86e 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt @@ -61,7 +61,7 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.Swap, is TransactionType.Transfer, is TransactionType.UnknownOperation, - TransactionType.YieldSupply.Send, + is TransactionType.YieldSupply.Send, TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } @@ -83,10 +83,12 @@ internal class TxHistoryItemToTransactionStateConverter( is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) - TransactionType.YieldSupply.Send -> if (isOutgoing) { - resourceReference(R.string.common_transfer) - } else { - resourceReference(R.string.yield_module_transaction_withdraw) + is TransactionType.YieldSupply.Send -> { + if (type.isYieldSupplyWithdraw || isOutgoing) { + resourceReference(R.string.yield_module_transaction_withdraw) + } else { + resourceReference(R.string.common_transfer) + } } is TransactionType.YieldSupply.DeployContract -> resourceReference( R.string @@ -107,7 +109,7 @@ internal class TxHistoryItemToTransactionStateConverter( private fun TxInfo.extractSubtitle(): TextReference { return when (val type = this.type) { is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) { - if (type == TransactionType.YieldSupply.Send) { + if (type is TransactionType.YieldSupply.Send) { extractSubtitleByAddressType() } else { resourceReference( @@ -129,8 +131,8 @@ internal class TxHistoryItemToTransactionStateConverter( val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount)) } - TransactionType.YieldSupply.Send -> { - if (isOutgoing) { + is TransactionType.YieldSupply.Send -> { + if (isOutgoing || !type.isYieldSupplyWithdraw) { extractSubtitleByAddressType() } else { val amount = @@ -190,12 +192,9 @@ internal class TxHistoryItemToTransactionStateConverter( TransactionType.Staking.Withdraw, -> return "" - is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Token) { - when (type) { - TransactionType.YieldSupply.Send -> if (!isOutgoing) { - return "" - } - else -> return "" + is TransactionType.YieldSupply -> { + if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) { + return "" } } else -> Unit diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index a30612c67e..b04f228e55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -71,7 +71,7 @@ internal class TxHistoryItemStateConverter( is TransactionType.Swap, is TransactionType.Transfer, is TransactionType.UnknownOperation, - TransactionType.YieldSupply.Send, + is TransactionType.YieldSupply.Send, TransactionType.YieldSupply.Topup, -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 } @@ -93,10 +93,12 @@ internal class TxHistoryItemStateConverter( is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter) is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit) TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup) - TransactionType.YieldSupply.Send -> if (isOutgoing) { - resourceReference(R.string.common_transfer) - } else { - resourceReference(R.string.yield_module_transaction_withdraw) + is TransactionType.YieldSupply.Send -> { + if (type.isYieldSupplyWithdraw || isOutgoing) { + resourceReference(R.string.yield_module_transaction_withdraw) + } else { + resourceReference(R.string.common_transfer) + } } is TransactionType.YieldSupply.DeployContract -> resourceReference( R.string @@ -137,8 +139,8 @@ internal class TxHistoryItemStateConverter( wrappedList(amount), ) } - TransactionType.YieldSupply.Send -> { - if (isOutgoing) { + is TransactionType.YieldSupply.Send -> { + if (isOutgoing || !type.isYieldSupplyWithdraw) { extractSubtitleByAddressType() } else { val amount = From 22e756b4effaf04f481ee48265ca092c393df855 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 11:03:01 +0300 Subject: [PATCH 28/33] Updated on 2026-08-14 --- .../tokenreceive/entity/TokenReceiveStateFactory.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt index ee3dd1a028..bcd955a4af 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt @@ -6,13 +6,10 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.domain.models.ReceiveAddressModel -import com.tangem.domain.models.TokenReceiveType import com.tangem.domain.models.TokenReceiveNotification +import com.tangem.domain.models.TokenReceiveType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.ens.EnsAddress import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens @@ -111,7 +108,10 @@ internal class TokenReceiveStateFactory( } ReceiveAddressModel.NameService.Ens -> Ens ReceiveAddressModel.NameService.Legacy -> Primary.Legacy( - displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address), + displayName = resourceReference( + R.string.domain_receive_assets_legacy_address, + WrappedList(listOf(cryptoCurrency.name)), + ), ) } ReceiveAddress( From 032fbe824a39af4002413f77ee40915be5bc8ae9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 14:47:02 +0500 Subject: [PATCH 29/33] Updated on 2026-08-14 --- .../impl/presentation/state/utils/StakingRewardsUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt index 454a1aa5fb..21af44fe79 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt @@ -47,7 +47,7 @@ internal fun getRewardScheduleText( -> getCustomRewardSchedule( networkId = networkId, decapitalize = decapitalize, - ) + ) ?: stringReference(rewardSchedule.name.lowercase().capitalize()) else -> null } } From afddda1198b47e221a28d3443abb0a0a678c15c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 13:58:25 +0400 Subject: [PATCH 30/33] Updated on 2026-08-14 --- .../ui/tokens/TokenItemStateConverter.kt | 3 + .../token/internal/YieldSupplyPromoBanner.kt | 2 +- .../YieldSupplyPromoBannerKeyConverter.kt | 9 ++- .../YieldSupplyPromoBannerKeyConverterTest.kt | 70 +++++++++++++++++-- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 029babd400..38a1c8c270 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -397,6 +397,9 @@ class TokenItemStateConverter( onYieldPromoCloseClick: (() -> Unit)?, ): TokenItemState.PromoBannerState { val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty + if (status.value !is CryptoCurrencyStatus.Loaded) { + return TokenItemState.PromoBannerState.Empty + } if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() || yieldModuleApyMap[token.yieldSupplyKey()] == null ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt index 31ddd112b2..b949061451 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt @@ -44,8 +44,8 @@ internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: M Row( modifier = Modifier .background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium) - .padding(horizontal = 12.dp, vertical = 8.dp) .clickable(onClick = state.onPromoBannerClick) + .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt index dd2569e500..f335bb30d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt @@ -20,16 +20,15 @@ internal class YieldSupplyPromoBannerKeyConverter( is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies() is TokenConverterParams.Account -> value.accountList.flattenCurrencies() }.filter { status -> - status.value is CryptoCurrencyStatus.Loaded || - status.value is CryptoCurrencyStatus.Custom + status.value is CryptoCurrencyStatus.Loaded } - val tokens = currencies.filter { it.currency is CryptoCurrency.Token } + val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token } - if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null if (yieldModuleApyMap.isEmpty()) return null - val max = tokens.asSequence() + val max = cryptoCurrencyStatuses.asSequence() .mapNotNull { status -> val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null val tokenKey = token.yieldSupplyKey() diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt index 458cbcaef2..3631ab69c0 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -20,7 +20,7 @@ class YieldSupplyPromoBannerKeyConverterTest { @Test fun `GIVEN promo disabled WHEN convert THEN return null`() { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF") - val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) + val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) val tokenList = ungroupedTokenList(status) val params = TokenConverterParams.Wallet( portfolioId = PortfolioId.Wallet(UserWalletId("00")), @@ -39,7 +39,7 @@ class YieldSupplyPromoBannerKeyConverterTest { @Test fun `GIVEN empty apy map WHEN convert THEN return null`() { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") - val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) + val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( portfolioId = PortfolioId.Wallet(UserWalletId("00")), tokenList = ungroupedTokenList(status), @@ -57,7 +57,7 @@ class YieldSupplyPromoBannerKeyConverterTest { @Test fun `GIVEN active yield token present WHEN convert THEN return null`() { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") - val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) + val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) val params = TokenConverterParams.Wallet( portfolioId = PortfolioId.Wallet(UserWalletId("00")), tokenList = ungroupedTokenList(statusActive), @@ -78,8 +78,8 @@ class YieldSupplyPromoBannerKeyConverterTest { val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd") val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF") - val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false) - val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) + val statusSmall = createLoadedStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false) + val statusBig = createLoadedStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) val apyMap = mapOf( "${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), @@ -105,7 +105,7 @@ class YieldSupplyPromoBannerKeyConverterTest { fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() { val nonEvmId = "xrp" val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123") - val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) + val status = createLoadedStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}" val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) @@ -124,6 +124,24 @@ class YieldSupplyPromoBannerKeyConverterTest { assertThat(result).isNull() } + @Test + fun `GIVEN custom status WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM") + val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped { return TokenList.Ungrouped( totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded( @@ -135,7 +153,45 @@ class YieldSupplyPromoBannerKeyConverterTest { ) } - private fun createStatus( + private fun createLoadedStatus( + token: CryptoCurrency.Token, + amount: BigDecimal, + isYieldActive: Boolean, + ): CryptoCurrencyStatus { + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "addr", + type = NetworkAddress.Address.Type.Primary, + ), + ) + val value = CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = BigDecimal.ZERO, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + yieldBalance = null, + yieldSupplyStatus = if (isYieldActive) { + YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ) + } else { + null + }, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = networkAddress, + sources = CryptoCurrencyStatus.Sources(), + ) + return CryptoCurrencyStatus( + currency = token, + value = value, + ) + } + + private fun createCustomStatus( token: CryptoCurrency.Token, amount: BigDecimal, isYieldActive: Boolean, From 7b574b2a20c58137d45d361e579c3c7521dc7545 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 15:29:07 +0400 Subject: [PATCH 31/33] Updated on 2026-08-14 --- .../currency/CryptoCurrencyExtensions.kt | 8 +- .../currency/CryptoCurrencyExtensionsTest.kt | 417 ++++++++++++++++++ .../YieldSupplyGetDustMinAmountUseCase.kt | 7 + .../YieldSupplyActiveMinAmountTransformer.kt | 10 +- .../impl/main/model/YieldSupplyModel.kt | 4 +- 5 files changed, 435 insertions(+), 11 deletions(-) create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index 09c43362de..cc408905ee 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -7,16 +7,16 @@ fun CryptoCurrency.Token.yieldSupplyKey(): String { } fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean { - val notSupplied = notSuppliedAmountOrNull() ?: return false + val notSupplied = notSuppliedCryptoAmountOrNull() ?: return false return notSupplied > BigDecimal.ZERO } -fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount: BigDecimal): Boolean { - val notSupplied = notSuppliedAmountOrNull() ?: return false +fun CryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustAmount: BigDecimal): Boolean { + val notSupplied = notSuppliedCryptoAmountOrNull()?.multiply(this.value.fiatRate) ?: return false return notSupplied >= dustAmount } -fun CryptoCurrencyStatus.notSuppliedAmountOrNull(): BigDecimal? { +fun CryptoCurrencyStatus.notSuppliedCryptoAmountOrNull(): BigDecimal? { if (this.currency !is CryptoCurrency.Token) return null val supplyStatus = this.value.yieldSupplyStatus diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt new file mode 100644 index 0000000000..402352cc42 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt @@ -0,0 +1,417 @@ +package com.tangem.domain.models.currency + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.yield.supply.YieldSupplyStatus +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CryptoCurrencyExtensionsTest { + + @Test + fun `GIVEN currency is Coin WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN yieldSupplyStatus is null WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = null, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN yieldSupplyStatus isActive is false WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = false, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN effectiveProtocolBalance is null WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ), + amount = BigDecimal.TEN, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN amount is null WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + amount = null, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN notSupplied is zero WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + amount = BigDecimal.TEN, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN notSupplied is negative WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + amount = BigDecimal.ONE, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN notSupplied is positive WHEN hasNotSuppliedAmount THEN returns true`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.ONE, + ), + amount = BigDecimal.TEN, + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isTrue() + } + + @Test + fun `GIVEN currency is Coin WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN yieldSupplyStatus is null WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = null, + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN yieldSupplyStatus isActive is false WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = false, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN effectiveProtocolBalance is null WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ), + amount = BigDecimal.TEN, + fiatRate = BigDecimal.ONE, + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN notSupplied in fiat is less than dustAmount WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal("9"), + ), + amount = BigDecimal.TEN, + fiatRate = BigDecimal.ONE, + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN notSupplied in fiat equals dustAmount WHEN shouldShowNotSuppliedNotification THEN returns true`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal("5"), + ), + amount = BigDecimal.TEN, + fiatRate = BigDecimal("2"), + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN) + + assertThat(result).isTrue() + } + + @Test + fun `GIVEN notSupplied in fiat is greater than dustAmount WHEN shouldShowNotSuppliedNotification THEN returns true`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.ONE, + ), + amount = BigDecimal.TEN, + fiatRate = BigDecimal("2"), + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN) + + assertThat(result).isTrue() + } + + @Test + fun `GIVEN USDT with EUR fiat rate and notSupplied below dust WHEN shouldShowNotSuppliedNotification THEN returns false`() { + val usdtToEurRate = BigDecimal("0.93") + val notSuppliedUsdt = BigDecimal("0.05") + val protocolBalance = BigDecimal("100") + val totalAmount = protocolBalance.add(notSuppliedUsdt) + val dustAmountEur = BigDecimal("0.1") + + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = protocolBalance, + ), + amount = totalAmount, + fiatRate = usdtToEurRate, + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = dustAmountEur) + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN USDT with EUR fiat rate and notSupplied above dust WHEN shouldShowNotSuppliedNotification THEN returns true`() { + val usdtToEurRate = BigDecimal("0.93") + val notSuppliedUsdt = BigDecimal("0.15") + val protocolBalance = BigDecimal("100") + val totalAmount = protocolBalance.add(notSuppliedUsdt) + val dustAmountEur = BigDecimal("0.1") + + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = protocolBalance, + ), + amount = totalAmount, + fiatRate = usdtToEurRate, + ) + + val result = status.shouldShowNotSuppliedNotification(dustAmount = dustAmountEur) + + assertThat(result).isTrue() + } + + @Test + fun `GIVEN currency is Coin WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN yieldSupplyStatus is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = null, + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN yieldSupplyStatus isActive is false WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = false, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN effectiveProtocolBalance is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ), + amount = BigDecimal.TEN, + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN amount is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal.TEN, + ), + amount = null, + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN valid data WHEN notSuppliedCryptoAmountOrNull THEN returns amount minus protocolBalance`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + yieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = BigDecimal("3"), + ), + amount = BigDecimal.TEN, + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isEqualTo(BigDecimal("7")) + } + + private fun createCryptoCurrencyStatus( + currency: CryptoCurrency, + yieldSupplyStatus: YieldSupplyStatus? = null, + amount: BigDecimal? = null, + fiatRate: BigDecimal? = null, + ): CryptoCurrencyStatus { + val value = mockk { + every { this@mockk.yieldSupplyStatus } returns yieldSupplyStatus + every { this@mockk.amount } returns amount + every { this@mockk.fiatRate } returns fiatRate + } + return CryptoCurrencyStatus(currency = currency, value = value) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt index 7814d60add..941311c2fa 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt @@ -3,6 +3,13 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal +/** + * Use case for getting dust minimum amount for yield supply. + * + * Returns the minimum amount based on the selected [AppCurrency]. + * If [AppCurrency] is in [SUPPORTED_DUST_CURRENCIES], returns [DUST_MIN_AMOUNT], + * otherwise returns the original [minAmount] with trailing zeros stripped. + */ class YieldSupplyGetDustMinAmountUseCase { operator fun invoke(minAmount: BigDecimal, appCurrency: AppCurrency): BigDecimal { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt index 54dd971067..6350642aae 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt @@ -11,8 +11,8 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.currency.notSuppliedAmountOrNull -import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon +import com.tangem.domain.models.currency.notSuppliedCryptoAmountOrNull +import com.tangem.domain.models.currency.shouldShowNotSuppliedNotification import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM @@ -91,11 +91,11 @@ internal class YieldSupplyActiveMinAmountTransformer( } private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? { - return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustMinAmount)) { + return if (cryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustMinAmount)) { val cryptoCurrency = cryptoCurrencyStatus.currency - val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull() + val notSuppliedAmount = cryptoCurrencyStatus.notSuppliedCryptoAmountOrNull() val formattedAmount = - notDepositedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) } + notSuppliedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) } analyticsHandler.send( YieldSupplyAnalytics.NoticeAmountNotDeposited( token = cryptoCurrency.symbol, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index c6bb211f29..9cd65e6cb7 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -19,7 +19,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.hasNotSuppliedAmount -import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon +import com.tangem.domain.models.currency.shouldShowNotSuppliedNotification import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.yield.supply.YieldSupplyStatus import com.tangem.domain.networks.single.SingleNetworkStatusFetcher @@ -351,7 +351,7 @@ internal class YieldSupplyModel @Inject constructor( minAmount = minAmount, appCurrency = appCurrency, ) - cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount) + cryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustAmount) } else { false } From c3850e7991119e0aea51d552ddf06d92fe7d383d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 18:09:53 +0400 Subject: [PATCH 32/33] Updated on 2026-08-14 --- .../YieldSupplyGetDustMinAmountUseCase.kt | 9 ++- .../usecase/YieldSupplyMinAmountUseCase.kt | 8 ++ .../YieldSupplyGetDustMinAmountUseCaseTest.kt | 74 +++++++++++++++++-- .../active/model/YieldSupplyActiveModel.kt | 6 +- .../impl/main/model/YieldSupplyModel.kt | 3 +- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt index 941311c2fa..54ee458d9d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import java.math.BigDecimal /** @@ -12,11 +13,15 @@ import java.math.BigDecimal */ class YieldSupplyGetDustMinAmountUseCase { - operator fun invoke(minAmount: BigDecimal, appCurrency: AppCurrency): BigDecimal { + operator fun invoke( + minAmountTokenCurrency: BigDecimal, + appCurrency: AppCurrency, + tokenCryptoCurrencyStatus: CryptoCurrencyStatus, + ): BigDecimal { return if (appCurrency.code in SUPPORTED_DUST_CURRENCIES) { DUST_MIN_AMOUNT } else { - minAmount.stripTrailingZeros() + minAmountTokenCurrency.multiply(tokenCryptoCurrencyStatus.value.fiatRate) } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt index 501e6c74f5..ccb1f53f0d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt @@ -13,6 +13,14 @@ import com.tangem.domain.yield.supply.fixFee import java.math.BigDecimal import java.math.RoundingMode +/** + * Use case that calculates the minimum amount required for yield supply operations. + * + * The calculation is based on the estimated transaction fee converted to the token currency, + * with a buffer multiplier applied to account for fee fluctuations. + * + * @return [BigDecimal] minimum amount in token currency (not native/network currency) + */ class YieldSupplyMinAmountUseCase( private val feeRepository: FeeRepository, private val quotesRepository: QuotesRepository, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt index 50a00ac69c..39017ac168 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt @@ -2,6 +2,10 @@ package com.tangem.domain.yield.supply.usecase import com.google.common.truth.Truth.assertThat import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress import org.junit.jupiter.api.Test import java.math.BigDecimal @@ -13,19 +17,79 @@ class YieldSupplyGetDustMinAmountUseCaseTest { fun `GIVEN supported currency WHEN invoke THEN return dust min amount`() { val minAmount = BigDecimal("123.456") val appCurrency = AppCurrency(code = "EUR", name = "Euro", symbol = "€") + val tokenStatus = createTokenStatus(fiatRate = BigDecimal("2.0")) - val result = useCase(minAmount, appCurrency) + val result = useCase(minAmount, appCurrency, tokenStatus) assertThat(result).isEqualTo(BigDecimal("0.1")) } @Test - fun `GIVEN unsupported currency WHEN invoke THEN return min amount stripped`() { - val minAmount = BigDecimal("1.2300") + fun `GIVEN unsupported currency WHEN invoke THEN return min amount multiplied by fiat rate`() { + val minAmount = BigDecimal("1.23") + val fiatRate = BigDecimal("150.0") val appCurrency = AppCurrency(code = "JPY", name = "Japanese Yen", symbol = "¥") + val tokenStatus = createTokenStatus(fiatRate = fiatRate) - val result = useCase(minAmount, appCurrency) + val result = useCase(minAmount, appCurrency, tokenStatus) - assertThat(result).isEqualTo(BigDecimal("1.23")) + assertThat(result).isEqualTo(minAmount.multiply(fiatRate)) + } + + private fun createTokenStatus(fiatRate: BigDecimal): CryptoCurrencyStatus { + val network = createNetwork() + val token = createToken(network) + return CryptoCurrencyStatus( + currency = token, + value = CryptoCurrencyStatus.Loaded( + amount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + } + + private fun createNetwork(): Network { + val derivationPath = Network.DerivationPath.None + return Network( + id = Network.ID(Network.RawID("polygon"), derivationPath), + backendId = "polygon", + name = "Polygon", + currencySymbol = "MATIC", + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.ENS, + ) + } + + private fun createToken(network: Network): CryptoCurrency.Token { + val tokenId = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(network.rawId), + suffix = CryptoCurrency.ID.Suffix.RawID("test-token", "0xContract"), + ) + return CryptoCurrency.Token( + id = tokenId, + network = network, + name = "Test Token", + symbol = "TT", + decimals = 18, + iconUrl = null, + isCustom = false, + contractAddress = "0xContract", + ) } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index e18a49c84a..3c65576a24 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -242,7 +242,11 @@ internal class YieldSupplyActiveModel @Inject constructor( userWalletId, cryptoCurrencyStatusFlow.value, ).onRight { minAmount -> - val dustAmount = yieldSupplyGetDustMinAmountUseCase(minAmount = minAmount, appCurrency = appCurrency) + val dustAmount = yieldSupplyGetDustMinAmountUseCase( + minAmountTokenCurrency = minAmount, + appCurrency = appCurrency, + tokenCryptoCurrencyStatus = cryptoCurrencyStatusFlow.value, + ) uiState.update( YieldSupplyActiveMinAmountTransformer( cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 9cd65e6cb7..c9e5d75c69 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -348,8 +348,9 @@ internal class YieldSupplyModel @Inject constructor( .getOrNull() if (minAmount != null) { val dustAmount = yieldSupplyGetDustMinAmountUseCase( - minAmount = minAmount, + minAmountTokenCurrency = minAmount, appCurrency = appCurrency, + tokenCryptoCurrencyStatus = cryptoCurrencyStatus, ) cryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustAmount) } else { From 195ffd4391aa31532ef26abaa51d6a00b414a6ff Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 20:31:57 +0500 Subject: [PATCH 33/33] Updated on 2026-08-14 --- .../api/common/response/ApiResponseError.kt | 48 +++++++++++++++++++ .../repository/TangemPayRequestPerformer.kt | 5 +- .../data/pay/util/TangemPayErrorConverter.kt | 1 + .../com/tangem/domain/visa/error/VisaError.kt | 1 + 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index 8b818cff1b..9d8e0aa9c4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -18,6 +18,54 @@ sealed class ApiResponseError : Exception() { val errorBody: String?, ) : ApiResponseError() { + fun isServerError(): Boolean = when (code) { + Code.OK, + Code.CREATED, + Code.ACCEPTED, + Code.NOT_MODIFIED, + Code.BAD_REQUEST, + Code.UNAUTHORIZED, + Code.PAYMENT_REQUIRED, + Code.FORBIDDEN, + Code.NOT_FOUND, + Code.METHOD_NOT_ALLOWED, + Code.NOT_ACCEPTABLE, + Code.PROXY_AUTHENTICATION_REQUIRED, + Code.REQUEST_TIMEOUT, + Code.CONFLICT, + Code.GONE, + Code.LENGTH_REQUIRED, + Code.PRECONDITION_FAILED, + Code.PAYLOAD_TOO_LARGE, + Code.URI_TOO_LONG, + Code.UNSUPPORTED_MEDIA_TYPE, + Code.RANGE_NOT_SATISFIABLE, + Code.EXPECTATION_FAILED, + Code.IM_A_TEAPOT, + Code.UNPROCESSABLE_ENTITY, + Code.LOCKED, + Code.FAILED_DEPENDENCY, + Code.TOO_EARLY, + Code.UPGRADE_REQUIRED, + Code.PRECONDITION_REQUIRED, + Code.TOO_MANY_REQUESTS, + Code.REQUEST_HEADER_FIELDS_TOO_LARGE, + Code.UNAVAILABLE_FOR_LEGAL_REASONS, + -> false + Code.INTERNAL_SERVER_ERROR, + Code.NOT_IMPLEMENTED, + Code.BAD_GATEWAY, + Code.SERVICE_UNAVAILABLE, + Code.GATEWAY_TIMEOUT, + Code.HTTP_VERSION_NOT_SUPPORTED, + Code.VARIANT_ALSO_NEGOTIATES, + Code.INSUFFICIENT_STORAGE, + Code.LOOP_DETECTED, + Code.NOT_EXTENDED, + Code.NETWORK_AUTHENTICATION_REQUIRED, + -> true + } + // TODO: extract Code from HttpException // region Error Codes enum class Code(val numericCode: Int) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 788aafe9d8..13229e7dc9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -159,10 +159,11 @@ internal class TangemPayRequestPerformer @Inject constructor( refreshToken = response.refreshToken, refreshExpiresAt = response.refreshExpiresAt, ) + }.mapLeft { error -> + Timber.tag(TAG).e("Can not refresh auth tokens: $error") + if (error is VisaApiError.ServerUnavailable) error else VisaApiError.RefreshTokenExpired }.onRight { tokens -> tangemPayStorage.storeAuthTokens(customerWalletAddress = customerWalletAddress, tokens = tokens) - }.onLeft { - Timber.tag(TAG).e("Can not refresh auth tokens: $it") } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index 8080f3b642..58659ca2ca 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -18,6 +18,7 @@ internal class TangemPayErrorConverter @Inject constructor( override fun convert(value: Throwable): VisaApiError { return if (value is ApiResponseError.HttpException) { + if (value.isServerError()) return VisaApiError.ServerUnavailable if (value.code == ApiResponseError.HttpException.Code.NOT_FOUND) return VisaApiError.NotPaeraCustomer if (value.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) return VisaApiError.RefreshTokenExpired diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index 4321710d19..412045caff 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -65,6 +65,7 @@ sealed class VisaApiError( data object WithdrawalDataError : VisaApiError(104004003) data object SignWithdrawError : VisaApiError(104004004) data object WithdrawError : VisaApiError(104004005) + data object ServerUnavailable : VisaApiError(104004006) companion object { fun fromBackendError(backendErrorCode: Int): VisaApiError {