From c9917f1c053bfa34dd207d0ac8dc157a09df9316 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 May 2023 20:27:46 +0300 Subject: [PATCH 001/100] Updated on 2026-08-14 --- .../tangem/tap/features/demo/DemoHelper.kt | 13 ++-- .../wallet/saltPay/GnosisRegistrator.kt | 14 ++-- .../features/send/redux/SendScreenAction.kt | 3 +- .../redux/middlewares/RequestFeeMiddleware.kt | 20 +++-- .../send/redux/reducers/FeeReducer.kt | 76 ++++++++++--------- .../features/send/redux/states/FeeState.kt | 3 +- .../CurrencyExchangeManager.kt | 7 +- .../tap/proxy/TransactionManagerImpl.kt | 73 ++++++++++++------ 8 files changed, 128 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index cccc4167ae..8191c1016f 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult @@ -84,14 +85,14 @@ object DemoHelper { class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { - override suspend fun getFee(amount: Amount, destination: String): Result> { + override suspend fun getFee(amount: Amount, destination: String): Result { val blockchain = walletManager.wallet.blockchain return Result.Success( - listOf( - Amount(0.0001.toBigDecimal(), blockchain), - Amount(0.0002.toBigDecimal(), blockchain), - Amount(0.0003.toBigDecimal(), blockchain), - ), + TransactionFee.Choosable( + minimum = Amount(0.0001.toBigDecimal(), blockchain), + normal = Amount(0.0002.toBigDecimal(), blockchain), + priority = Amount(0.0003.toBigDecimal(), blockchain) + ) ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index b5dfe53817..4eab5e464d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.successOr @@ -205,13 +206,16 @@ class GnosisRegistrator( } @Suppress("MagicNumber") - private fun Result>.extractFeeAmount(): Result { + private fun Result.extractFeeAmount(): Result { return when (this) { is Result.Success -> { - if (this.data.size != 3) { - Result.Failure(BlockchainSdkError.FailedToLoadFee) - } else { - Result.Success(this.data[1]) + when(data) { + is TransactionFee.Single -> { + Result.Failure(BlockchainSdkError.FailedToLoadFee) + } + is TransactionFee.Choosable -> { + Result.Success((data as TransactionFee.Choosable).normal) + } } } is Result.Failure -> this diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 719cd930c3..8034231609 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -4,6 +4,7 @@ import com.tangem.Message import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.StateDialog @@ -127,7 +128,7 @@ sealed class FeeAction : SendScreenAction { object RequestFee : FeeAction() sealed class FeeCalculation : FeeAction() { - data class SetFeeResult(val fee: List) : FeeCalculation() + data class SetFeeResult(val fee: TransactionFee) : FeeCalculation() object ClearResult : FeeCalculation() } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 8a0b096c4c..49749c36f8 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.send.redux.middlewares import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.AppState @@ -53,15 +54,18 @@ class RequestFeeMiddleware { val result = feeResult.data // val result = FeeMock.getFee(walletManager.wallet.blockchain) dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) - if (result.size == 1) { - val fee = result[0].value ?: BigDecimal.ZERO - if (fee.isZero()) { - dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - } else { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false)) + when(result) { + is TransactionFee.Single -> { + val fee = result.value.value ?: BigDecimal.ZERO + if (fee.isZero()) { + dispatch(FeeAction.ChangeLayoutVisibility(main = false)) + } else { + dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false)) + } + } + is TransactionFee.Choosable -> { + dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true)) } - } else { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true)) } } is Result.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index d0548132b9..b7f943a4e4 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.reducers import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.FeeActionUi import com.tangem.tap.features.send.redux.SendScreenAction @@ -13,6 +14,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState [REDACTED_AUTHOR] */ class FeeReducer : SendInternalReducer { + override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) { is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState) is FeeAction -> handleAction(action, sendState, sendState.feeState) @@ -25,11 +27,13 @@ class FeeReducer : SendInternalReducer { state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible) } is FeeActionUi.ChangeSelectedFee -> { - val currentFee = createValueOfFeeAmount(action.feeType, state.feeList) - state.copy( - selectedFeeType = action.feeType, - currentFee = currentFee, - ) + state.feeList?.let { + val currentFee = createValueOfFeeAmount(action.feeType, it) + state.copy( + selectedFeeType = action.feeType, + currentFee = currentFee, + ) + } ?: state } is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded) } @@ -51,26 +55,29 @@ class FeeReducer : SendInternalReducer { } is FeeAction.FeeCalculation.SetFeeResult -> { val fees = action.fee - if (fees.size == 1) { - val feeType = FeeType.SINGLE - val currentFee = createValueOfFeeAmount(feeType, fees) + when (fees) { + is TransactionFee.Single -> { + val feeType = FeeType.SINGLE + val currentFee = createValueOfFeeAmount(feeType, fees) - state.copy( - selectedFeeType = feeType, - feeList = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) - } else { - val feeType = getCurrentFeeType(state) - val currentFee = createValueOfFeeAmount(feeType, fees) + state.copy( + selectedFeeType = feeType, + feeList = fees, + currentFee = currentFee, + feeIsApproximate = isFeeApproximate(sendState), + ) + } + is TransactionFee.Choosable -> { + val feeType = getCurrentFeeType(state) + val currentFee = createValueOfFeeAmount(feeType, fees) - state.copy( - selectedFeeType = feeType, - feeList = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) + state.copy( + selectedFeeType = feeType, + feeList = fees, + currentFee = currentFee, + feeIsApproximate = isFeeApproximate(sendState), + ) + } }.copy( progressState = ProgressState.Done, ) @@ -87,17 +94,18 @@ class FeeReducer : SendInternalReducer { return updateLastState(sendState.copy(feeState = result), result) } - private fun createValueOfFeeAmount(feeType: FeeType, list: List?): Amount? { - if (list == null || list.isEmpty()) return null - - return if (list.size == 1) { - list[0] - } else { - when (feeType) { - FeeType.SINGLE -> list[1] - FeeType.LOW -> list[0] - FeeType.NORMAL -> list[1] - FeeType.PRIORITY -> list[2] + private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Amount { + return when (transactionFee) { + is TransactionFee.Single -> { + transactionFee.value + } + is TransactionFee.Choosable -> { + when (feeType) { + FeeType.SINGLE -> transactionFee.normal + FeeType.LOW -> transactionFee.minimum + FeeType.NORMAL -> transactionFee.normal + FeeType.PRIORITY -> transactionFee.priority + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index e9c9472bb4..aa0824a755 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.states import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.wallet.redux.ProgressState import java.math.BigDecimal @@ -13,7 +14,7 @@ enum class FeeType { data class FeeState( val selectedFeeType: FeeType = FeeType.NORMAL, - val feeList: List? = null, + val feeList: TransactionFee? = null, val currentFee: Amount? = null, val feeIsIncluded: Boolean = false, val feeIsApproximate: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index a73937a8ee..6be6ffc898 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -72,7 +72,7 @@ class CurrencyExchangeManager( return when (action) { Action.Buy -> buyService Action.Sell -> sellService - } as ExchangeUrlBuilder + } } enum class Action { Buy, Sell } @@ -96,12 +96,11 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( val amountToSend = Amount(walletManager.wallet.blockchain) val destinationAddress = token.contractAddress - val feeResult = - walletManager.getFee( + val feeResult = walletManager.getFee( amountToSend, destinationAddress, ) as? Result.Success ?: return - val fee = feeResult.data[0] + val fee = feeResult.data.minimum val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO if (coinValue < fee.value) return diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index dabd3c53ec..9ff4ceecd2 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.isNetworkError @@ -197,24 +198,37 @@ class TransactionManagerImpl( return when (fee) { is Result.Success -> { // for not EVM blockchains set gasLimit ZERO for now - val firstFee = fee.data.firstOrNull() ?: error("no fee found") - val minFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = firstFee), - ) - val normalFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: firstFee), - ) - val priorityFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 2) ?: firstFee), - ) - ProxyFees( - minFee = minFee, - normalFee = normalFee, - priorityFee = priorityFee, - ) + when (fee.data) { + is TransactionFee.Single -> { + val singleFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = (fee.data as TransactionFee.Single).value), + ) + ProxyFees( + minFee = singleFee, + normalFee = singleFee, + priorityFee = singleFee + ) + } + is TransactionFee.Choosable -> { + val setOfThree = fee.data as TransactionFee.Choosable + ProxyFees( + minFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = setOfThree.minimum), + ), + normalFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = setOfThree.normal), + ), + priorityFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = setOfThree.priority), + ) + ) + } + } + } is Result.Failure -> { error(fee.error.message ?: fee.error.customMessage) @@ -263,18 +277,33 @@ class TransactionManagerImpl( } return when (fee) { is Result.Success -> { - val minFee = fee.data.firstOrNull() ?: error("no fee found") + val choosableFee = fee.data + ProxyFees( + minFee = ProxyFee( + gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + fee = convertToProxyAmount(choosableFee.minimum), + ), + normalFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = choosableFee.normal), + ), + priorityFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = choosableFee.priority), + ) + ) + val minProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(minFee), + fee = convertToProxyAmount(fee.data.minimum), ) val normalProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: minFee), + fee = convertToProxyAmount(fee.data.normal), ) val priorityProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.lastOrNull() ?: minFee), + fee = convertToProxyAmount(fee.data.priority), ) ProxyFees( minFee = minProxyFee, From 5e4d8a9113866bb2ac867b73cf74409b923cf422 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 May 2023 14:44:10 +0300 Subject: [PATCH 002/100] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/demo/DemoHelper.kt | 10 +++++++--- .../products/wallet/saltPay/GnosisRegistrator.kt | 3 ++- .../tap/features/send/redux/reducers/FeeReducer.kt | 6 ++++-- .../com/tangem/tap/proxy/TransactionManagerImpl.kt | 3 ++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 8191c1016f..56a60b96a3 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -89,9 +89,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact val blockchain = walletManager.wallet.blockchain return Result.Success( TransactionFee.Choosable( - minimum = Amount(0.0001.toBigDecimal(), blockchain), - normal = Amount(0.0002.toBigDecimal(), blockchain), - priority = Amount(0.0003.toBigDecimal(), blockchain) + minimum = Amount(minimumFee, blockchain), + normal = Amount(normalFee, blockchain), + priority = Amount(priorityFee, blockchain) ) ) } @@ -121,5 +121,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact companion object { val ID = DemoTransactionSender::class.java.simpleName + + private val minimumFee = 0.0001.toBigDecimal() + private val normalFee = 0.0002.toBigDecimal() + private val priorityFee = 0.0003.toBigDecimal() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index 4eab5e464d..fb513d2e11 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -214,7 +214,8 @@ class GnosisRegistrator( Result.Failure(BlockchainSdkError.FailedToLoadFee) } is TransactionFee.Choosable -> { - Result.Success((data as TransactionFee.Choosable).normal) + val normalFee = (data as TransactionFee.Choosable).normal + Result.Success(normalFee) } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index b7f943a4e4..0c285c2fa9 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -41,6 +41,7 @@ class FeeReducer : SendInternalReducer { } private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState { + val result = when (action) { is FeeAction.RequestFee -> { state.copy(progressState = ProgressState.Loading) @@ -54,8 +55,7 @@ class FeeReducer : SendInternalReducer { ) } is FeeAction.FeeCalculation.SetFeeResult -> { - val fees = action.fee - when (fees) { + when (val fees = action.fee) { is TransactionFee.Single -> { val feeType = FeeType.SINGLE val currentFee = createValueOfFeeAmount(feeType, fees) @@ -120,4 +120,6 @@ class FeeReducer : SendInternalReducer { private fun getCurrentFeeType(state: FeeState): FeeType { return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType } + + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 9ff4ceecd2..3d489d18fe 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -200,9 +200,10 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { + val amount = (fee.data as TransactionFee.Single).value val singleFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = (fee.data as TransactionFee.Single).value), + fee = convertToProxyAmount(amount = amount), ) ProxyFees( minFee = singleFee, From c8acba2f3bc6d829788bed918b12d73ad80c181b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 May 2023 13:58:21 +0300 Subject: [PATCH 003/100] Updated on 2026-08-14 --- .../tap/features/send/redux/middlewares/RequestFeeMiddleware.kt | 2 +- .../com/tangem/tap/features/send/redux/reducers/FeeReducer.kt | 2 +- .../main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 49749c36f8..4e68e14449 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -56,7 +56,7 @@ class RequestFeeMiddleware { dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) when(result) { is TransactionFee.Single -> { - val fee = result.value.value ?: BigDecimal.ZERO + val fee = result.normal.value ?: BigDecimal.ZERO if (fee.isZero()) { dispatch(FeeAction.ChangeLayoutVisibility(main = false)) } else { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index 0c285c2fa9..58506a51be 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -97,7 +97,7 @@ class FeeReducer : SendInternalReducer { private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Amount { return when (transactionFee) { is TransactionFee.Single -> { - transactionFee.value + transactionFee.normal } is TransactionFee.Choosable -> { when (feeType) { diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 3d489d18fe..c1a687a860 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -200,7 +200,7 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { - val amount = (fee.data as TransactionFee.Single).value + val amount = (fee.data as TransactionFee.Single).normal val singleFee = ProxyFee( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = amount), From cc4b2e829952748bc06382b8e599642cecba3eff Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 May 2023 18:22:10 +0300 Subject: [PATCH 004/100] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index c1a687a860..7c511dbceb 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -279,33 +279,20 @@ class TransactionManagerImpl( return when (fee) { is Result.Success -> { val choosableFee = fee.data - ProxyFees( - minFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(choosableFee.minimum), - ), - normalFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.normal), - ), - priorityFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.priority), - ) - ) val minProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.minimum), + fee = convertToProxyAmount(amount = choosableFee.minimum), ) val normalProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.normal), + fee = convertToProxyAmount(amount = choosableFee.normal), ) val priorityProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.priority), + fee = convertToProxyAmount(amount = choosableFee.priority), ) + ProxyFees( minFee = minProxyFee, normalFee = normalProxyFee, From 85ea643f3be6eeaa62b0d5caf8785fc861c18e5e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Jun 2023 14:31:43 +0300 Subject: [PATCH 005/100] Updated on 2026-08-14 --- .../features/send/redux/reducers/FeeReducer.kt | 15 ++++++++++----- .../tap/features/send/redux/states/FeeState.kt | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index 58506a51be..4c41cf308a 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -8,6 +8,7 @@ import com.tangem.tap.features.send.redux.SendScreenAction import com.tangem.tap.features.send.redux.states.FeeState import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.tap.features.send.redux.states.TransactionExtrasState import com.tangem.tap.features.wallet.redux.ProgressState /** @@ -27,17 +28,21 @@ class FeeReducer : SendInternalReducer { state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible) } is FeeActionUi.ChangeSelectedFee -> { - state.feeList?.let { + state.fees?.let { val currentFee = createValueOfFeeAmount(action.feeType, it) state.copy( selectedFeeType = action.feeType, currentFee = currentFee, ) } ?: state + // TODO? } is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded) } - return updateLastState(sendState.copy(feeState = result), result) + return updateLastState(sendState.copy( + feeState = result, + transactionExtrasState = TransactionExtrasState() + ), result) } private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState { @@ -62,7 +67,7 @@ class FeeReducer : SendInternalReducer { state.copy( selectedFeeType = feeType, - feeList = fees, + fees = fees, currentFee = currentFee, feeIsApproximate = isFeeApproximate(sendState), ) @@ -73,7 +78,7 @@ class FeeReducer : SendInternalReducer { state.copy( selectedFeeType = feeType, - feeList = fees, + fees = fees, currentFee = currentFee, feeIsApproximate = isFeeApproximate(sendState), ) @@ -84,7 +89,7 @@ class FeeReducer : SendInternalReducer { } FeeAction.FeeCalculation.ClearResult -> { state.copy( - feeList = null, + fees = null, currentFee = null, progressState = ProgressState.Done, ) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index aa0824a755..d1191cf2a4 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -14,7 +14,7 @@ enum class FeeType { data class FeeState( val selectedFeeType: FeeType = FeeType.NORMAL, - val feeList: TransactionFee? = null, + val fees: TransactionFee? = null, val currentFee: Amount? = null, val feeIsIncluded: Boolean = false, val feeIsApproximate: Boolean = false, From fa6b33840198278b59cfdebd9f4ce2e47ffed0b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Jun 2023 17:18:37 +0300 Subject: [PATCH 006/100] Updated on 2026-08-14 --- .../com/tangem/tap/features/demo/DemoHelper.kt | 7 ++++--- .../wallet/saltPay/GnosisRegistrator.kt | 2 +- .../redux/middlewares/RequestFeeMiddleware.kt | 2 +- .../features/send/redux/reducers/FeeReducer.kt | 10 +++++----- .../CurrencyExchangeManager.kt | 2 +- .../tangem/tap/proxy/TransactionManagerImpl.kt | 18 +++++++++--------- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 56a60b96a3..205029fe66 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -89,9 +90,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact val blockchain = walletManager.wallet.blockchain return Result.Success( TransactionFee.Choosable( - minimum = Amount(minimumFee, blockchain), - normal = Amount(normalFee, blockchain), - priority = Amount(priorityFee, blockchain) + minimum = Fee(Amount(minimumFee, blockchain)), + normal = Fee(Amount(normalFee, blockchain)), + priority = Fee(Amount(priorityFee, blockchain)) ) ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index fb513d2e11..2205efdac9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -215,7 +215,7 @@ class GnosisRegistrator( } is TransactionFee.Choosable -> { val normalFee = (data as TransactionFee.Choosable).normal - Result.Success(normalFee) + Result.Success(normalFee.amount) } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 4e68e14449..1277c0993b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -56,7 +56,7 @@ class RequestFeeMiddleware { dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) when(result) { is TransactionFee.Single -> { - val fee = result.normal.value ?: BigDecimal.ZERO + val fee = result.normal.amount.value ?: BigDecimal.ZERO if (fee.isZero()) { dispatch(FeeAction.ChangeLayoutVisibility(main = false)) } else { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index 4c41cf308a..317929dbcf 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -102,14 +102,14 @@ class FeeReducer : SendInternalReducer { private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Amount { return when (transactionFee) { is TransactionFee.Single -> { - transactionFee.normal + transactionFee.normal.amount } is TransactionFee.Choosable -> { when (feeType) { - FeeType.SINGLE -> transactionFee.normal - FeeType.LOW -> transactionFee.minimum - FeeType.NORMAL -> transactionFee.normal - FeeType.PRIORITY -> transactionFee.priority + FeeType.SINGLE -> transactionFee.normal.amount + FeeType.LOW -> transactionFee.minimum.amount + FeeType.NORMAL -> transactionFee.normal.amount + FeeType.PRIORITY -> transactionFee.priority.amount } } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 6be6ffc898..8b66738ac9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -100,7 +100,7 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( amountToSend, destinationAddress, ) as? Result.Success ?: return - val fee = feeResult.data.minimum + val fee = feeResult.data.minimum.amount val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO if (coinValue < fee.value) return diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 7c511dbceb..a18da39a15 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -200,10 +200,10 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { - val amount = (fee.data as TransactionFee.Single).normal + val fee = (fee.data as TransactionFee.Single).normal val singleFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = amount), + fee = convertToProxyAmount(amount = fee.amount), ) ProxyFees( minFee = singleFee, @@ -212,19 +212,19 @@ class TransactionManagerImpl( ) } is TransactionFee.Choosable -> { - val setOfThree = fee.data as TransactionFee.Choosable + val choosableFee = fee.data as TransactionFee.Choosable ProxyFees( minFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = setOfThree.minimum), + fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ), normalFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = setOfThree.normal), + fee = convertToProxyAmount(amount = choosableFee.normal.amount), ), priorityFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = setOfThree.priority), + fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) ) } @@ -282,15 +282,15 @@ class TransactionManagerImpl( val minProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.minimum), + fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) val normalProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.normal), + fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) val priorityProxyFee = ProxyFee( gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.priority), + fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) ProxyFees( From 564e558fccbe670d645cb742cee37408b37ae76f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Jun 2023 12:23:48 +0300 Subject: [PATCH 007/100] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 6 +++--- .../wallet/saltPay/GnosisRegistrator.kt | 5 +++-- .../send/redux/middlewares/AmountMiddleware.kt | 2 +- .../send/redux/middlewares/SendMiddleware.kt | 17 +++++++++-------- .../features/send/redux/reducers/FeeReducer.kt | 13 +++++++------ .../tap/features/send/redux/states/FeeState.kt | 5 +++-- .../exchangeServices/CurrencyExchangeManager.kt | 4 ++-- .../tangem/tap/proxy/TransactionManagerImpl.kt | 10 ++++++---- 8 files changed, 34 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 5ccf53357d..ef6ba6e420 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.hexToBigDecimal @@ -85,7 +86,7 @@ class WalletConnectSdkHelper { val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), - fee = Amount(fee, wallet.blockchain), + fee = Fee(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = transaction.to!!, extras = EthereumTransactionExtras( @@ -194,8 +195,7 @@ class WalletConnectSdkHelper { val dataToSign = EthereumUtils.buildTransactionToSign( transactionData = data.transaction, nonce = null, - blockchain = data.walletManager.wallet.blockchain, - gasLimit = null, + blockchain = data.walletManager.wallet.blockchain ) ?: return null val command = SignHashCommand( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index 2205efdac9..ed29935f3e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -139,7 +140,7 @@ class GnosisRegistrator( val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign( transactionData = walletManager.createTransaction( amount = approveAmount, - fee = feeAmount, + fee = Fee(feeAmount), destination = otpProcessorContractAddress, ), nonce = atomicNonce.getAndIncrement().toBigInteger(), @@ -190,7 +191,7 @@ class GnosisRegistrator( val transactionData = walletManager.createTransferFromTransaction( amount = amount, - fee = hardcodeFeeAmount, + feeAmount = hardcodeFeeAmount, source = addressTreasureSafe, ) val transactionToSign = walletManager.transactionBuilder.buildTransferFromToSign( diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt index 15522e87d2..0564218bcd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt @@ -67,7 +67,7 @@ class AmountMiddleware { } val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto)) - val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee) + val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee?.amount) val amountFieldErrors = filterErrorsForAmountField(transactionErrors) if (amountFieldErrors.isEmpty()) { dispatch(AmountAction.SetAmountError(null)) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 2d22340b26..679da154f1 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -12,6 +12,7 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionError import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard @@ -118,11 +119,11 @@ private fun verifyAndSendTransaction( val card = appState.globalState.scanResponse?.card ?: return val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return val typedAmount = sendState.amountState.amountToExtract ?: return - val feeAmount = sendState.feeState.currentFee ?: return + val currentFee = sendState.feeState.currentFee ?: return val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend()) - val transactionErrors = walletManager.validateTransaction(amountToSend, feeAmount) + val transactionErrors = walletManager.validateTransaction(amountToSend, currentFee.amount) when { transactionErrors.contains(TransactionError.TezosSendAll) -> { val reduceAmount = walletManager.wallet.blockchain.minimalAmount() @@ -134,7 +135,7 @@ private fun verifyAndSendTransaction( }, sendAllCallback = { sendTransaction( - action, walletManager, amountToSend, feeAmount, destinationAddress, + action, walletManager, amountToSend, currentFee, destinationAddress, sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch, ) @@ -145,7 +146,7 @@ private fun verifyAndSendTransaction( } else -> { sendTransaction( - action, walletManager, amountToSend, feeAmount, destinationAddress, + action, walletManager, amountToSend, currentFee, destinationAddress, sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch, ) } @@ -157,7 +158,7 @@ private fun sendTransaction( action: SendActionUi.SendAmountToRecipient, walletManager: WalletManager, amountToSend: Amount, - feeAmount: Amount, + fee: Fee, destinationAddress: String, transactionExtras: TransactionExtrasState, card: CardDTO, @@ -165,7 +166,7 @@ private fun sendTransaction( dispatch: (Action) -> Unit, ) { dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS)) - var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress) + var txData = walletManager.createTransaction(amountToSend, fee, destinationAddress) transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) } transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) } @@ -185,7 +186,7 @@ private fun sendTransaction( updateFeedbackManagerInfo( walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + feeAmount = fee.amount, destinationAddress = destinationAddress, ) dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) @@ -266,7 +267,7 @@ private fun sendTransaction( updateFeedbackManagerInfo( walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + feeAmount = fee.amount, destinationAddress = destinationAddress, ) val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index 317929dbcf..b2a4e5b947 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.reducers import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.FeeActionUi @@ -99,17 +100,17 @@ class FeeReducer : SendInternalReducer { return updateLastState(sendState.copy(feeState = result), result) } - private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Amount { + private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Fee { return when (transactionFee) { is TransactionFee.Single -> { - transactionFee.normal.amount + transactionFee.normal } is TransactionFee.Choosable -> { when (feeType) { - FeeType.SINGLE -> transactionFee.normal.amount - FeeType.LOW -> transactionFee.minimum.amount - FeeType.NORMAL -> transactionFee.normal.amount - FeeType.PRIORITY -> transactionFee.priority.amount + FeeType.SINGLE -> transactionFee.normal + FeeType.LOW -> transactionFee.minimum + FeeType.NORMAL -> transactionFee.normal + FeeType.PRIORITY -> transactionFee.priority } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index d1191cf2a4..4fd9516f9a 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.states import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.wallet.redux.ProgressState import java.math.BigDecimal @@ -15,7 +16,7 @@ enum class FeeType { data class FeeState( val selectedFeeType: FeeType = FeeType.NORMAL, val fees: TransactionFee? = null, - val currentFee: Amount? = null, + val currentFee: Fee? = null, val feeIsIncluded: Boolean = false, val feeIsApproximate: Boolean = false, val mainLayoutIsVisible: Boolean = false, @@ -29,5 +30,5 @@ data class FeeState( fun isReady(): Boolean = currentFee != null - fun getCurrentFeeValue(): BigDecimal = currentFee?.value ?: BigDecimal.ZERO + fun getCurrentFeeValue(): BigDecimal = currentFee?.amount?.value ?: BigDecimal.ZERO } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 8b66738ac9..b04348a966 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -100,10 +100,10 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( amountToSend, destinationAddress, ) as? Result.Success ?: return - val fee = feeResult.data.minimum.amount + val fee = feeResult.data.minimum val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO - if (coinValue < fee.value) return + if (coinValue < fee.amount.value) return val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index a18da39a15..5c18b45cdf 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.EthereumFeeExtras +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -99,7 +101,7 @@ class TransactionManagerImpl( ): SendTxResult { val txData = walletManager.createTransaction( amount = amount, - fee = Amount(value = feeAmount, blockchain = blockchain), + fee = Fee(Amount(value = feeAmount, blockchain = blockchain)), destination = destinationAddress, ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) @@ -281,15 +283,15 @@ class TransactionManagerImpl( val choosableFee = fee.data val minProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + gasLimit = (choosableFee.minimum.extras as EthereumFeeExtras).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) val normalProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + gasLimit = (choosableFee.normal.extras as EthereumFeeExtras).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) val priorityProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + gasLimit = (choosableFee.priority.extras as EthereumFeeExtras).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) From 3a9073f9f28cc9516f5db4c3a844127513e7eb3a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Jun 2023 18:40:45 +0300 Subject: [PATCH 008/100] Updated on 2026-08-14 --- .../send/redux/reducers/FeeReducer.kt | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index b2a4e5b947..ba706cf281 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -29,21 +29,17 @@ class FeeReducer : SendInternalReducer { state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible) } is FeeActionUi.ChangeSelectedFee -> { - state.fees?.let { - val currentFee = createValueOfFeeAmount(action.feeType, it) - state.copy( - selectedFeeType = action.feeType, - currentFee = currentFee, - ) - } ?: state - // TODO? + val currentFee = state.fees?.let { + createValueOfFeeAmount(action.feeType, it) + } + state.copy( + selectedFeeType = action.feeType, + currentFee = currentFee + ) } is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded) } - return updateLastState(sendState.copy( - feeState = result, - transactionExtrasState = TransactionExtrasState() - ), result) + return updateLastState(sendState.copy(feeState = result), result) } private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState { From 72256ab5c58a82552f87d2ccbfc8eb0326a042ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Jun 2023 09:27:20 +0300 Subject: [PATCH 009/100] Updated on 2026-08-14 --- .../tap/domain/walletconnect/WalletConnectSdkHelper.kt | 2 +- .../main/java/com/tangem/tap/features/demo/DemoHelper.kt | 6 +++--- .../products/wallet/saltPay/GnosisRegistrator.kt | 2 +- .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 9 ++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index ef6ba6e420..a26050a16d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -86,7 +86,7 @@ class WalletConnectSdkHelper { val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), - fee = Fee(Amount(fee, wallet.blockchain)), + fee = Fee.CommonFee(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = transaction.to!!, extras = EthereumTransactionExtras( diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 205029fe66..2dacf8a93d 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -90,9 +90,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact val blockchain = walletManager.wallet.blockchain return Result.Success( TransactionFee.Choosable( - minimum = Fee(Amount(minimumFee, blockchain)), - normal = Fee(Amount(normalFee, blockchain)), - priority = Fee(Amount(priorityFee, blockchain)) + minimum = Fee.CommonFee(Amount(minimumFee, blockchain)), + normal = Fee.CommonFee(Amount(normalFee, blockchain)), + priority = Fee.CommonFee(Amount(priorityFee, blockchain)) ) ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index ed29935f3e..126dd5d655 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -140,7 +140,7 @@ class GnosisRegistrator( val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign( transactionData = walletManager.createTransaction( amount = approveAmount, - fee = Fee(feeAmount), + fee = Fee.CommonFee(feeAmount), destination = otpProcessorContractAddress, ), nonce = atomicNonce.getAndIncrement().toBigInteger(), diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 5c18b45cdf..12450303f6 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -6,7 +6,6 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.transaction.EthereumFeeExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result @@ -101,7 +100,7 @@ class TransactionManagerImpl( ): SendTxResult { val txData = walletManager.createTransaction( amount = amount, - fee = Fee(Amount(value = feeAmount, blockchain = blockchain)), + fee = Fee.CommonFee(Amount(value = feeAmount, blockchain = blockchain)), destination = destinationAddress, ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) @@ -283,15 +282,15 @@ class TransactionManagerImpl( val choosableFee = fee.data val minProxyFee = ProxyFee( - gasLimit = (choosableFee.minimum.extras as EthereumFeeExtras).gasLimit, + gasLimit = (choosableFee.minimum as Fee.EthereumFee).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) val normalProxyFee = ProxyFee( - gasLimit = (choosableFee.normal.extras as EthereumFeeExtras).gasLimit, + gasLimit = (choosableFee.normal as Fee.EthereumFee).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) val priorityProxyFee = ProxyFee( - gasLimit = (choosableFee.priority.extras as EthereumFeeExtras).gasLimit, + gasLimit = (choosableFee.priority as Fee.EthereumFee).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) From 24b9265c738f799942c5fba4f852b1dec28a5093 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Jun 2023 23:27:16 +0300 Subject: [PATCH 010/100] Updated on 2026-08-14 --- .../tap/domain/walletconnect/WalletConnectSdkHelper.kt | 2 +- .../main/java/com/tangem/tap/features/demo/DemoHelper.kt | 6 +++--- .../products/wallet/saltPay/GnosisRegistrator.kt | 2 +- .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index a26050a16d..af3a5c19f9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -86,7 +86,7 @@ class WalletConnectSdkHelper { val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), - fee = Fee.CommonFee(Amount(fee, wallet.blockchain)), + fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = transaction.to!!, extras = EthereumTransactionExtras( diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 2dacf8a93d..83ea251268 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -90,9 +90,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact val blockchain = walletManager.wallet.blockchain return Result.Success( TransactionFee.Choosable( - minimum = Fee.CommonFee(Amount(minimumFee, blockchain)), - normal = Fee.CommonFee(Amount(normalFee, blockchain)), - priority = Fee.CommonFee(Amount(priorityFee, blockchain)) + minimum = Fee.Common(Amount(minimumFee, blockchain)), + normal = Fee.Common(Amount(normalFee, blockchain)), + priority = Fee.Common(Amount(priorityFee, blockchain)) ) ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index 126dd5d655..fa982df1c9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -140,7 +140,7 @@ class GnosisRegistrator( val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign( transactionData = walletManager.createTransaction( amount = approveAmount, - fee = Fee.CommonFee(feeAmount), + fee = Fee.Common(feeAmount), destination = otpProcessorContractAddress, ), nonce = atomicNonce.getAndIncrement().toBigInteger(), diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 12450303f6..24b221cc39 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -100,7 +100,7 @@ class TransactionManagerImpl( ): SendTxResult { val txData = walletManager.createTransaction( amount = amount, - fee = Fee.CommonFee(Amount(value = feeAmount, blockchain = blockchain)), + fee = Fee.Common(Amount(value = feeAmount, blockchain = blockchain)), destination = destinationAddress, ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) @@ -282,15 +282,15 @@ class TransactionManagerImpl( val choosableFee = fee.data val minProxyFee = ProxyFee( - gasLimit = (choosableFee.minimum as Fee.EthereumFee).gasLimit, + gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) val normalProxyFee = ProxyFee( - gasLimit = (choosableFee.normal as Fee.EthereumFee).gasLimit, + gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) val priorityProxyFee = ProxyFee( - gasLimit = (choosableFee.priority as Fee.EthereumFee).gasLimit, + gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) From 77680682a64780ad361e09ae946ed713ba7f705b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Jun 2023 12:25:10 +0300 Subject: [PATCH 011/100] Updated on 2026-08-14 --- .../wallet/redux/reducers/WalletReducer.kt | 3 +- .../wallet/ui/MultipleAddressUiHelper.kt | 28 ++++--------------- gradle/dependencies.toml | 4 +-- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index a196a6146b..ac44f59413 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.common.redux.AppState @@ -151,7 +152,7 @@ fun Wallet.createAddressesData(): List { getShareUri(it.value), getExploreUrl(it.value), ) - if (it.type == blockchain.defaultAddressType()) { + if (it.type == AddressType.Default) { listOfAddressData.add(0, addressData) } else { listOfAddressData.add(addressData) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index 67e10ca9a6..3bd06aed6a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -1,40 +1,24 @@ package com.tangem.tap.features.wallet.ui -import android.view.View -import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType -import com.tangem.blockchain.blockchains.cardano.CardanoAddressType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.wallet.R object MultipleAddressUiHelper { + fun typeToId(type: AddressType): Int { return when (type) { - is BitcoinAddressType.Legacy -> R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID + AddressType.Legacy -> R.id.chip_legacy + AddressType.Default -> R.id.chip_default } } fun idToType(id: Int, blockchain: Blockchain?): AddressType? { return when (id) { - R.id.chip_default -> { - when (blockchain) { - Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.Litecoin -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley - else -> null - } - } - R.id.chip_legacy -> { - when (blockchain) { - Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.Litecoin -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron - else -> null - } - } + R.id.chip_default -> AddressType.Default + R.id.chip_legacy -> AddressType.Legacy else -> null } } + } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f7f6cb31df..1274465081 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -74,8 +74,8 @@ reactiveNetwork = "3.0.8" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-225" -#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds +#tangemBlockchainSdk = "develop-225" +tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-249" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From 907bc6335ef877ff7a638d6e2fa62dac43381dea Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 23 Jun 2023 16:39:25 +0300 Subject: [PATCH 012/100] Updated on 2026-08-14 --- .../tangem/tap/domain/extensions/WalletManagerFactory.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index a9de7cb47a..3d65b09b25 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -35,7 +35,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( val seedKey = wallet.extendedPublicKey return when { scanResponse.cardTypesResolver.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { - makeTwinWalletManager( + createTwinWalletManager( walletPublicKey = wallet.publicKey, pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(), blockchain = environmentBlockchain, @@ -51,7 +51,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( val derivedKey = derivedKeys?.get(derivationPath) ?: return null - makeWalletManager( + createWalletManager( blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, @@ -59,7 +59,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( ) } else -> { - makeWalletManager( + createLegacyWalletManager( blockchain = environmentBlockchain, walletPublicKey = wallet.publicKey, curve = wallet.curve, From 2ab957d18a5d5875ec27bbe80670bd9473309f65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Jun 2023 20:25:28 +0300 Subject: [PATCH 013/100] Updated on 2026-08-14 --- .../wallet/ui/MultipleAddressUiHelper.kt | 46 ++++++++++--------- .../wallet/ui/WalletDetailsFragment.kt | 2 +- .../wallet/ui/wallet/SingleWalletView.kt | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index e82c2f9547..b4866eda69 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -1,20 +1,24 @@ package com.tangem.tap.features.wallet.ui import android.view.View -import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType -import com.tangem.blockchain.blockchains.cardano.CardanoAddressType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.wallet.R object MultipleAddressUiHelper { - fun typeToId(type: AddressType): Int { - return when (type) { - is BitcoinAddressType.Legacy -> R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID + + fun typeToId(type: AddressType, blockchain: Blockchain): Int { + return when (blockchain) { + in blockchainsSupportingSplit -> { + if (type == AddressType.Legacy) { + R.id.chip_legacy + } else { + R.id.chip_default + } + } + else -> { + View.NO_ID + } } } @@ -22,27 +26,27 @@ object MultipleAddressUiHelper { return when (id) { R.id.chip_default -> { when (blockchain) { - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.Litecoin, - Blockchain.BitcoinCash, - -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley + in blockchainsSupportingSplit -> AddressType.Default else -> null } } + R.id.chip_legacy -> { when (blockchain) { - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.Litecoin, - Blockchain.BitcoinCash, - -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron + in blockchainsSupportingSplit -> AddressType.Legacy else -> null } } + else -> null } } + + private val blockchainsSupportingSplit = listOf( + Blockchain.Bitcoin, + Blockchain.BitcoinTestnet, + Blockchain.Litecoin, + Blockchain.BitcoinCash, + Blockchain.CardanoShelley + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index afee65593c..b8f1e06942 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -390,7 +390,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(selectedAddress.type) + val checkedId = MultipleAddressUiHelper.typeToId(selectedAddress.type, currency.blockchain) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) chipGroupAddressType.setOnCheckedChangeListener { _, checkedId -> diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index a97a94e9f3..b3ca7e7f2b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -190,7 +190,7 @@ class SingleWalletView : WalletView() { (binding.lAddress.root as? ViewGroup)?.beginDelayedTransition() chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(primaryWallet.walletAddresses.selectedAddress.type) + val checkedId = MultipleAddressUiHelper.typeToId(primaryWallet.walletAddresses.selectedAddress.type, primaryWallet.currency.blockchain) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> From e3db5621edd0088cb81aa27090e20c23321f8e75 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Jun 2023 23:44:45 +0300 Subject: [PATCH 014/100] Updated on 2026-08-14 --- .../wallet/ui/MultipleAddressUiHelper.kt | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index b4866eda69..7caaabe6e1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -22,22 +22,15 @@ object MultipleAddressUiHelper { } } - fun idToType(id: Int, blockchain: Blockchain?): AddressType? { - return when (id) { - R.id.chip_default -> { - when (blockchain) { - in blockchainsSupportingSplit -> AddressType.Default + fun idToType(id: Int, blockchain: Blockchain): AddressType? { + return when (blockchain) { + in blockchainsSupportingSplit -> { + when(id) { + R.id.chip_default -> AddressType.Default + R.id.chip_legacy -> AddressType.Legacy else -> null } } - - R.id.chip_legacy -> { - when (blockchain) { - in blockchainsSupportingSplit -> AddressType.Legacy - else -> null - } - } - else -> null } } @@ -49,4 +42,5 @@ object MultipleAddressUiHelper { Blockchain.BitcoinCash, Blockchain.CardanoShelley ) + } \ No newline at end of file From 59da7f01548f477f12b5372286341c3b7fcd9eb8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jul 2023 19:20:24 +0300 Subject: [PATCH 015/100] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index ab2ed7ee14..dec9b4f24f 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -1,7 +1,7 @@ [ { "name": "OPTIMISM_SWAP_FEATURE_ENABLED", - "version": "4.3.1" + "version": "1.0.0" }, { "name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED", From 2585f9e0e862bfb7b4c953cc696b5a9f9944e6db Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Jul 2023 16:31:20 +0300 Subject: [PATCH 016/100] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/TapApplication.kt | 11 +++++++--- .../java/com/tangem/tap/common/images/Coil.kt | 11 ++-------- .../walletconnect/WalletConnectManager.kt | 5 +++-- core/datasource/build.gradle.kts | 1 + .../tangem/datasource/api/common/Retrofit.kt | 20 ++++++++++--------- .../com/tangem/datasource/di/NetworkModule.kt | 4 +--- .../tangem/datasource/utils/HttpClientExt.kt | 11 ++++++---- gradle/dependencies.toml | 4 ++++ 9 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index efe2848d8c..14433e7634 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.walletConnectCore) implementation(deps.walletConnectWeb3) + implementation(deps.prettyLogger) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 114746ccdb..086d90c904 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory +import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.blockchain.common.BlockchainSdkConfig @@ -14,6 +15,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader @@ -58,7 +60,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.runBlocking -import okhttp3.logging.HttpLoggingInterceptor import org.rekotlin.Store import timber.log.Timber import javax.inject.Inject @@ -168,7 +169,11 @@ class TapApplication : Application(), ImageLoaderFactory { ) if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) + Timber.plant(object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + Logger.log(priority, tag, message, t) + } + }) } foregroundActivityObserver = ForegroundActivityObserver() @@ -187,7 +192,7 @@ class TapApplication : Application(), ImageLoaderFactory { if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, + createNetworkLoggingInterceptor() ) } diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index b7305e86dc..0d6d4de675 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -4,8 +4,8 @@ import android.content.Context import android.util.Log import coil.ImageLoader import coil.util.Logger +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber private const val COIL_LOG_TAG = "COIL" @@ -18,14 +18,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL logger(CoilTimberLogger()) okHttpClient { OkHttpClient.Builder() - .addNetworkInterceptor( - HttpLoggingInterceptor { message -> - Timber.tag(COIL_LOG_TAG).d(message) - } - .apply { - level = HttpLoggingInterceptor.Level.BODY - }, - ) + .addNetworkInterceptor(createNetworkLoggingInterceptor()) .build() } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index 7b80ce1039..99825e2721 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.WalletConnect @@ -36,7 +37,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber import java.util.* import java.util.concurrent.TimeUnit @@ -56,8 +56,9 @@ class WalletConnectManager { .addInterceptor(RetryInterceptor()) .build() } + private val interceptor by lazy { - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + createNetworkLoggingInterceptor() } private var sessions: MutableMap = mutableMapOf() diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index de4006b63a..114edb01aa 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(deps.retrofit) implementation(deps.retrofit.moshi) implementation(deps.reactive.network) + implementation(deps.prettyOkHttpLoggingInterceptor) /** Time */ implementation(deps.jodatime) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 166036783c..72aea8deb2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.common +import android.util.Log +import com.ihsanbal.logging.Level +import com.ihsanbal.logging.LoggingInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import java.util.concurrent.TimeUnit @@ -22,15 +24,15 @@ fun createRetrofitInstance( } interceptors.forEach { okHttpBuilder.addInterceptor(it) } - if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) + if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor()) - return Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(MoshiConverter.networkMoshiConverter) - .client(okHttpBuilder.build()) - .build() + return Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(MoshiConverter.networkMoshiConverter) + .client(okHttpBuilder.build()).build() } -private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY +fun createNetworkLoggingInterceptor(): Interceptor { + return LoggingInterceptor.Builder() + .setLevel(Level.BODY) + .log(Log.VERBOSE) + .build() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index c8ae82faa9..9c11afc771 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -63,9 +63,7 @@ class NetworkModule { @PromotionOneInch fun providePromotionOneInchApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): PromotionApi { val okClient = OkHttpClient.Builder() - .addHeaders( - AuthenticationHeader(authProvider), - ) + .addHeaders(AuthenticationHeader(authProvider)) .allowLogging() .callTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) .connectTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 4fd16a0d1d..384676621a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -1,10 +1,9 @@ package com.tangem.datasource.utils import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.logging.HttpLoggingInterceptor.Level /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder { @@ -26,5 +25,9 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade * * @param level logging level. By default, only the request body. */ -internal fun OkHttpClient.Builder.allowLogging(level: Level = Level.BODY): OkHttpClient.Builder = - if (BuildConfig.DEBUG) addInterceptor(interceptor = HttpLoggingInterceptor().setLevel(level)) else this \ No newline at end of file +internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder = + if (BuildConfig.DEBUG) { + addInterceptor(interceptor = createNetworkLoggingInterceptor()) + } else { + this + } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5fb4bbfb94..9b66d181ca 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -75,6 +75,8 @@ arrow = "1.2.0-RC" reactiveNetwork = "3.0.8" walletConnectCore = "1.15.0" walletConnectWeb3 = "1.8.0" +prettyLogger = "2.2.0" +prettyOkHttpLoggingInterceptor = "3.1.0" # endregion Other libraries # region Tangem @@ -222,4 +224,6 @@ arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", version.ref = "reactiveNetwork" } walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" } walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } +prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } +prettyOkHttpLoggingInterceptor = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "prettyOkHttpLoggingInterceptor" } # endregion Other From eafd243752d2b92a105855378b55f87b09e6d9c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Jul 2023 19:01:21 +0300 Subject: [PATCH 017/100] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 3 +-- .../main/java/com/tangem/datasource/api/common/Retrofit.kt | 7 +++++-- .../main/java/com/tangem/datasource/utils/HttpClientExt.kt | 7 ++++--- gradle/dependencies.toml | 5 ++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 114edb01aa..aa45701df5 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -32,11 +32,10 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.okHttp) - implementation(deps.okHttp.logging) + implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) implementation(deps.retrofit.moshi) implementation(deps.reactive.network) - implementation(deps.prettyOkHttpLoggingInterceptor) /** Time */ implementation(deps.jodatime) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 72aea8deb2..3d8b6f3930 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -26,8 +26,11 @@ fun createRetrofitInstance( if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor()) - return Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(MoshiConverter.networkMoshiConverter) - .client(okHttpBuilder.build()).build() + return Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(MoshiConverter.networkMoshiConverter) + .client(okHttpBuilder.build()) + .build() } fun createNetworkLoggingInterceptor(): Interceptor { diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 384676621a..f32626bf66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -25,9 +25,10 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade * * @param level logging level. By default, only the request body. */ -internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder = - if (BuildConfig.DEBUG) { +internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder { + return if (BuildConfig.DEBUG) { addInterceptor(interceptor = createNetworkLoggingInterceptor()) } else { this - } \ No newline at end of file + } +} diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 9b66d181ca..210582bd45 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -76,7 +76,7 @@ reactiveNetwork = "3.0.8" walletConnectCore = "1.15.0" walletConnectWeb3 = "1.8.0" prettyLogger = "2.2.0" -prettyOkHttpLoggingInterceptor = "3.1.0" +okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem @@ -203,7 +203,7 @@ material = { module = "com.google.android.material:material", version.ref = "goo moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } -okHttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } otaliastudiosCameraView = { module = "com.otaliastudios:cameraview", version.ref = "otaliastudiosCameraView" } shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } @@ -225,5 +225,4 @@ reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", versio walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" } walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } -prettyOkHttpLoggingInterceptor = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "prettyOkHttpLoggingInterceptor" } # endregion Other From 5b0d90152ed22f281614c313ff2c09ec5b2c9931 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Jul 2023 19:01:35 +0300 Subject: [PATCH 018/100] Updated on 2026-08-14 --- app/build.gradle.kts | 7 ------- domain/legacy/build.gradle.kts | 6 ------ 2 files changed, 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14433e7634..507d70e3b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,13 +114,6 @@ dependencies { implementation(deps.appsflyer) implementation(deps.amplitude) implementation(deps.kotsonGson) - //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(deps.retrofit) - implementation(deps.retrofit.moshi) - implementation(deps.moshi) - implementation(deps.moshi.kotlin) - implementation(deps.okHttp) - implementation(deps.okHttp.logging) implementation(deps.zendesk.chat) implementation(deps.zendesk.messaging) implementation(deps.spongecastle.core) diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f618e11fd9..320f10a255 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -22,12 +22,6 @@ dependencies { /** Other libraries */ implementation(deps.reKotlin) - //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(deps.retrofit) - implementation(deps.retrofit.moshi) - implementation(deps.moshi) - implementation(deps.moshi.kotlin) - implementation(deps.okHttp.logging) implementation(deps.timber) implementation(deps.kotlin.coroutines) From 0c638c7c96bae786f1213e8552d4fea2c457ecff Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 10 Jul 2023 12:17:27 +0300 Subject: [PATCH 019/100] Updated on 2026-08-14 --- domain/legacy/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 320f10a255..e9449ad7de 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { /** Other libraries */ implementation(deps.reKotlin) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) implementation(deps.timber) implementation(deps.kotlin.coroutines) From 86ca9ebbc98b6dfbb3ca7046785901f224295eb1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 10 Jul 2023 13:02:08 +0300 Subject: [PATCH 020/100] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TapApplication.kt | 14 ++++++++------ .../com/tangem/datasource/utils/HttpClientExt.kt | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 4d7bc9bd30..9c3d7f67cb 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -174,11 +174,13 @@ class TapApplication : Application(), ImageLoaderFactory { ) if (BuildConfig.DEBUG) { - Timber.plant(object : Timber.DebugTree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - Logger.log(priority, tag, message, t) - } - }) + Timber.plant( + object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + Logger.log(priority, tag, message, t) + } + }, + ) } foregroundActivityObserver = ForegroundActivityObserver() @@ -197,7 +199,7 @@ class TapApplication : Application(), ImageLoaderFactory { if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( - createNetworkLoggingInterceptor() + createNetworkLoggingInterceptor(), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index f32626bf66..4a5a6a1b75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -31,4 +31,4 @@ internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder { } else { this } -} +} \ No newline at end of file From 9076665333f79b8feba2de90cb5058be2818130e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jul 2023 12:49:48 +0300 Subject: [PATCH 021/100] Updated on 2026-08-14 --- .../com/tangem/domain/common/extensions/Blockchain.kt | 8 +------- gradle/dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 15fb408bde..f4084f38df 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -29,7 +29,6 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet "cardano" -> Blockchain.CardanoShelley "dogecoin" -> Blockchain.Dogecoin - "ducatus" -> Blockchain.Ducatus "litecoin" -> Blockchain.Litecoin "rootstock" -> Blockchain.RSK "stellar" -> Blockchain.Stellar @@ -87,7 +86,6 @@ fun Blockchain.toNetworkId(): String { Blockchain.Cardano -> "cardano" Blockchain.CardanoShelley -> "cardano" Blockchain.Dogecoin -> "dogecoin" - Blockchain.Ducatus -> "ducatus" Blockchain.Ethereum -> "ethereum" Blockchain.EthereumTestnet -> "ethereum/test" Blockchain.EthereumClassic -> "ethereum-classic" @@ -152,7 +150,6 @@ fun Blockchain.toCoinId(): String { Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom" Blockchain.Tron, Blockchain.TronTestnet -> "tron" Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot" - Blockchain.Ducatus -> "ducatus" Blockchain.Litecoin -> "litecoin" Blockchain.RSK -> "rootstock" Blockchain.Tezos -> "tezos" @@ -182,7 +179,4 @@ fun Blockchain.isSupportedInApp(): Boolean { return !excludedBlockchains.contains(this) } -private val excludedBlockchains = listOf( - Blockchain.Unknown, - Blockchain.Ducatus, -) \ No newline at end of file +private val excludedBlockchains = listOf(Blockchain.Unknown) \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 515c8832d2..042782007e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -78,7 +78,7 @@ walletConnectWeb3 = "1.10.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-268" +tangemBlockchainSdk = "develop-264" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-266" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 5624193e7b8879b79fbef1530150813a5e1d6a05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 10 Jul 2023 15:25:39 +0400 Subject: [PATCH 022/100] Updated on 2026-08-14 --- .../tap/domain/tasks/product/ResetToFactorySettingsTask.kt | 3 +-- .../src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt | 2 +- gradle/dependencies.toml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt index 1ab2b50b69..a2622b218d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -31,8 +31,7 @@ class ResetToFactorySettingsTask : CardSessionRunnable { } private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val backupStatus = session.environment.card?.backupStatus - if (backupStatus == null || backupStatus == Card.BackupStatus.NoBackup) { + if (session.environment.card?.backupStatus?.isActive != true) { callback(CompletionResult.Success(session.environment.card!!)) return } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 7578791134..a89909b584 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -304,7 +304,7 @@ data class CardDTO( object NoBackup : BackupStatus() val isActive: Boolean - get() = this is Active || this is CardLinked + get() = this is Active companion object { internal fun fromSdkStatus(sdkStatus: Card.BackupStatus?): BackupStatus? { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 042782007e..23948fc2c6 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ walletConnectWeb3 = "1.10.0" # region Tangem tangemBlockchainSdk = "develop-264" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-266" +tangemCardSdk = "develop-273" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From c698ca194cc85932f277c219b854a1b66aced24e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jul 2023 20:11:39 +0800 Subject: [PATCH 023/100] Updated on 2026-08-14 --- core/analytics/models/.gitignore | 1 + core/analytics/models/build.gradle.kts | 4 ++++ .../java/com/tangem/core/analytics/models}/AnalyticsEvent.kt | 2 +- settings.gradle.kts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 core/analytics/models/.gitignore create mode 100644 core/analytics/models/build.gradle.kts rename core/analytics/{src/main/java/com/tangem/core/analytics => models/src/main/java/com/tangem/core/analytics/models}/AnalyticsEvent.kt (81%) diff --git a/core/analytics/models/.gitignore b/core/analytics/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/analytics/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics/models/build.gradle.kts b/core/analytics/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/core/analytics/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt similarity index 81% rename from core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt rename to core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 1fbc89d977..e60b14d1ee 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.core.analytics +package com.tangem.core.analytics.models /** [REDACTED_AUTHOR] diff --git a/settings.gradle.kts b/settings.gradle.kts index 523476db95..9a14394b73 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ include(":common") // region Core modules include(":core:analytics") +include(":core:analytics:models") include(":core:datasource") include(":core:featuretoggles") include(":core:navigation") From b5ad0f297270b749fcef5c745f24d0ebe5bcd715 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jul 2023 20:19:14 +0800 Subject: [PATCH 024/100] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/common/analytics/events/Basic.kt | 2 +- .../tap/common/analytics/events/Chat.kt | 2 +- .../common/analytics/events/DetailsScreen.kt | 2 +- .../analytics/events/IntroductionProcess.kt | 2 +- .../tap/common/analytics/events/MainScreen.kt | 3 +-- .../common/analytics/events/ManageTokens.kt | 2 +- .../tap/common/analytics/events/MyWallets.kt | 2 +- .../tap/common/analytics/events/Onboarding.kt | 20 +------------------ .../tap/common/analytics/events/Portfolio.kt | 2 +- .../tap/common/analytics/events/Settings.kt | 2 +- .../tap/common/analytics/events/Shop.kt | 2 +- .../tap/common/analytics/events/SignIn.kt | 2 +- .../tap/common/analytics/events/Token.kt | 2 +- .../common/analytics/events/WalletConnect.kt | 2 +- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 2 +- .../firebase/FirebaseAnalyticsHandler.kt | 2 +- .../CardContextInterceptor.kt | 2 +- .../LinkedCardContextInterceptor.kt | 4 ++-- .../domain/scanCard/LegacyScanProcessor.kt | 2 +- .../tap/domain/scanCard/ScanCardProcessor.kt | 2 +- .../domain/scanCard/UseCaseScanProcessor.kt | 2 +- .../domain/scanCard/chains/AnalyticsChain.kt | 2 +- .../tap/features/home/redux/HomeAction.kt | 2 +- .../tap/features/home/redux/HomeMiddleware.kt | 2 +- .../tap/features/wallet/redux/WalletAction.kt | 2 +- core/analytics/build.gradle.kts | 4 +++- .../com/tangem/core/analytics/Analytics.kt | 15 +++----------- .../core/analytics/api/EventFilterApi.kt | 2 +- .../core/analytics/api/EventHandlerApi.kt | 2 +- .../analytics/api/ParamsInterceptorApi.kt | 2 +- domain/card/build.gradle.kts | 6 ++++-- features/learn2earn/impl/build.gradle.kts | 1 + .../learn2earn/analytics/Learn2earnEvents.kt | 2 +- .../domain/api/WebViewResultHandler.kt | 2 +- features/onboarding/build.gradle.kts | 1 + .../wallet2/analytics/SeedPhraseEvents.kt | 2 +- .../referral/presentation/build.gradle.kts | 1 + .../referral/analytics/ReferralEvents.kt | 2 +- features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/analytics/SwapEvents.kt | 4 +--- features/wallet/impl/build.gradle.kts | 3 +++ 42 files changed, 52 insertions(+), 70 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df6d24b384..ca27f5fd67 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(project(":domain:wallets:models")) implementation(project(":common")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:navigation")) implementation(project(":core:featuretoggles")) implementation(project(":core:res")) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index e5b89b30e5..2782ceac8f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt index 96fa307105..f4b9dbc61f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt index a0d34306c9..775f44a0d8 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt index 602365c0c6..b5ce0042ae 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt index 7a36f35d1e..fbbc88c099 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] @@ -13,7 +13,6 @@ sealed class MainScreen( class ScreenOpened : MainScreen("Screen opened") class ButtonScanCard : MainScreen("Button - Scan Card") class ButtonMyWallets : MainScreen("Button - My Wallets") - class ButtonBuy : MainScreen("Button - Buy") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index 8e0b8cc5b9..969d1d47fb 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.extensions.filterNotNull diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt index 65ab7937c2..87ba002047 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class MyWallets( event: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index 98dd408ba8..9612f2b972 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] @@ -72,26 +72,8 @@ sealed class Onboarding( class SetupFinished : Twins("Twin Setup Finished") } - class PinScreenOpened : Onboarding("Onboarding", "PIN screen opened") - class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code") - class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened") - class ButtonConnect : Onboarding("Onboarding", "Button - Connect") - class PinCodeSet : Onboarding("Onboarding", "PIN Code Set") - - class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened") - class KYCStarted : Onboarding("Onboarding", "KYC Started") - class KYCInProgress : Onboarding("Onboarding", "KYC In Progress") - class KYCRejected : Onboarding("Onboarding", "KYC Rejected") - - class ClaimScreenOpened : Onboarding("Onboarding", "Claim Screen Opened") - class ButtonClaim : Onboarding("Onboarding", "Button - Claim") - class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully") - class ButtonChat : Onboarding("Onboarding", "Button - Chat") - class NotEnoughGasError : Onboarding("Onboarding", "Not Enough Gas Error") - class CardNotPassedError : Onboarding("Onboarding", "Card Not Passed Error") - class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding( category = "Onboarding / Biometric", event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt index 71383c9f9a..3e961883b7 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 5a9226bec4..e71eda15ad 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.features.details.ui.details.SocialNetwork /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt index 0ab4dd9361..4076de8685 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index a5417d775b..de174fd193 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 837c9bce3c..9b8cecc417 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index 34047928e4..49c49b912a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index d1b3189ab1..c4b12fc278 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -1,8 +1,8 @@ package com.tangem.tap.common.analytics.handlers.appsFlyer import com.appsflyer.AFInAppEventType -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.events.Shop diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 7430fcc17f..e445597e8f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -1,9 +1,9 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.google.firebase.analytics.FirebaseAnalytics -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.ErrorEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter import com.tangem.tap.common.analytics.events.Shop diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 864c9dadbb..0db51098a3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt index 94c006d699..25b67c3ee0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt @@ -1,14 +1,14 @@ package com.tangem.tap.common.analytics.paramsInterceptor -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.models.scan.ScanResponse /** [REDACTED_AUTHOR] */ class LinkedCardContextInterceptor( - private val scanResponse: ScanResponse, + scanResponse: ScanResponse, val parent: LinkedCardContextInterceptor? = null, ) : ParamsInterceptor { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index e83d2496ff..e891d0dd2c 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -6,7 +6,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt index 0546a79db5..808f338f85 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -2,7 +2,7 @@ package com.tangem.tap.domain.scanCard import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index ae4e286e5f..002b9bc7ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.ScanCardException diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index febc6a1525..8f72ce92f8 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.Either import arrow.core.right import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 86b139defb..93479d1767 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.home.redux -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.IndeterminateProgressButton diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 7a77fcaa60..8a225afa8a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -5,7 +5,7 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 3f73a94c11..10803cf57f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.address.AddressType -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWallet diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index e1fb463a41..29b4cfb591 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -13,5 +13,7 @@ dependencies { kapt(deps.hilt.kapt) /** Core shouldn't depends on core, but in case with utils and logging its necessary */ - implementation(project(":core:utils")) + implementation(projects.core.utils) + + implementation(projects.core.analytics.models) } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 8dda5adedf..3c12cf80a8 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -1,18 +1,9 @@ package com.tangem.core.analytics -import com.tangem.core.analytics.api.AnalyticsEventFilter -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.api.AnalyticsFilterHolder -import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.api.AnalyticsHandlerHolder -import com.tangem.core.analytics.api.ParamsInterceptor -import com.tangem.core.analytics.api.ParamsInterceptorHolder +import com.tangem.core.analytics.api.* +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.util.concurrent.Executors /** diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt index 8381b2b983..4de7e4aee8 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 49aadf88c3..0a5f3179de 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt index 683a1c45a0..80b0bf2b14 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 213bd024b6..a1266a8ba6 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -4,9 +4,11 @@ plugins { } dependencies { - implementation(project(":domain:core")) + implementation(projects.domain.core) // TODO: Remove after new card scan result was implemented - implementation(project(":domain:models")) + implementation(projects.domain.models) + + implementation(projects.core.analytics.models) implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/features/learn2earn/impl/build.gradle.kts b/features/learn2earn/impl/build.gradle.kts index 26d0179187..37925318a5 100644 --- a/features/learn2earn/impl/build.gradle.kts +++ b/features/learn2earn/impl/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { /** Core modules */ implementation(project(":common")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:featuretoggles")) implementation(project(":core:datasource")) implementation(project(":core:utils")) diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt index 164fa4e793..100a19ef0a 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.learn2earn.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent internal sealed class Learn2earnEvents( category: String, diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt index 6b7c851a69..e9cc14f222 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt @@ -1,6 +1,6 @@ package com.tangem.feature.learn2earn.domain.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** * Handler that helps determine a result of the Learn2earnWebViewActivity webView actions. diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 1940a2073c..cdd735f59c 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(project(":core:featuretoggles")) implementation(project(":core:datasource")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:utils")) implementation(project(":core:ui")) implementation(project(":core:res")) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt index baf6d039cc..d593c179ed 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class SeedPhraseEvents( event: String, diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 1f66fc2708..0649e10587 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:res")) implementation(project(":core:utils")) implementation(project(":core:ui")) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index e82990adf2..a5d37a1b19 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.referral.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index f4a0857938..8248b21113 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:featuretoggles")) implementation(project(":core:utils")) implementation(project(":core:ui")) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index e808db167c..d2395cf29b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class SwapEvents( event: String, @@ -13,7 +13,6 @@ sealed class SwapEvents( ) object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - object ReceiveTokenClicked : SwapEvents(event = "Receive Token Clicked") object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened") object SearchTokenClicked : SwapEvents(event = "Searched Token Clicked") data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( @@ -24,7 +23,6 @@ sealed class SwapEvents( object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") object ButtonPermissionApproveClicked : SwapEvents(event = "Button - Permission Approve") object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - object ButtonPermitAndSwapClicked : SwapEvents(event = "Button - Permit and Swap") object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") object SwapInProgressScreen : SwapEvents(event = "Swap in Progress Screen Opened") } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 2e5651a89c..64139b1ecb 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) /** DI */ @@ -37,12 +38,14 @@ dependencies { implementation(project(":core:featuretoggles")) implementation(project(":core:navigation")) implementation(project(":core:ui")) + implementation(projects.core.utils) /** Feature Apis */ implementation(project(":features:wallet:api")) /** Domain modules */ implementation(project(":common")) + implementation(projects.domain.card) implementation(project(":domain:legacy")) implementation(project(":domain:models")) implementation(project(":domain:wallets")) From 80ac1d02c03e7d39ce01ccf3ddc90a74ec997dbb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 09:52:20 +0300 Subject: [PATCH 025/100] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/TokensDomainModule.kt | 26 ++++++++++++ data/tokens/.gitignore | 1 + data/tokens/build.gradle.kts | 17 ++++++++ .../tangem/data/tokens/di/TokensDataModule.kt | 30 +++++++++++++ .../repository/DefaultNetworksRepository.kt | 25 +++++++++++ .../repository/DefaultQuotesRepository.kt | 15 +++++++ .../repository/DefaultTokensRepository.kt | 32 ++++++++++++++ domain/tokens/.gitignore | 1 + domain/tokens/build.gradle.kts | 10 +++++ .../domain/tokens/GetTokenListUseCase.kt | 28 +++++++++++++ .../tangem/domain/tokens/error/TokensError.kt | 6 +++ .../com/tangem/domain/tokens/model/Network.kt | 10 +++++ .../domain/tokens/model/NetworkGroup.kt | 9 ++++ .../domain/tokens/model/NetworkStatus.kt | 23 ++++++++++ .../com/tangem/domain/tokens/model/Quote.kt | 9 ++++ .../com/tangem/domain/tokens/model/Token.kt | 14 +++++++ .../tangem/domain/tokens/model/TokenList.kt | 38 +++++++++++++++++ .../tangem/domain/tokens/model/TokenStatus.kt | 42 +++++++++++++++++++ .../tokens/repository/NetworksRepository.kt | 26 ++++++++++++ .../tokens/repository/QuotesRepository.kt | 18 ++++++++ .../tokens/repository/TokensRepository.kt | 29 +++++++++++++ features/wallet/impl/build.gradle.kts | 1 + .../router/DefaultWalletRouter.kt | 3 +- .../wallet/viewmodels/WalletViewModel.kt | 6 ++- settings.gradle.kts | 2 + 26 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt create mode 100644 data/tokens/.gitignore create mode 100644 data/tokens/build.gradle.kts create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt create mode 100644 domain/tokens/.gitignore create mode 100644 domain/tokens/build.gradle.kts create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b56b814c08..07f7cd8b81 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(project(":domain:card")) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.tokens) implementation(project(":common")) implementation(project(":core:analytics")) implementation(projects.core.analytics.models) @@ -40,6 +41,7 @@ dependencies { implementation(project(":libs:crypto")) implementation(project(":libs:auth")) implementation(project(":data:source:preferences")) + implementation(projects.data.tokens) /** Features */ implementation(project(":features:onboarding")) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt new file mode 100644 index 0000000000..bda1a7734d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TokensDomainModule { + + @Provides + @ViewModelScoped + fun provideGetTokenListUseCase( + tokensRepository: TokensRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ) = GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) +} \ No newline at end of file diff --git a/data/tokens/.gitignore b/data/tokens/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/tokens/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts new file mode 100644 index 0000000000..95abf3ed40 --- /dev/null +++ b/data/tokens/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +dependencies { + implementation(projects.domain.tokens) + implementation(projects.domain.wallets.models) + + implementation(deps.kotlin.coroutines) + + implementation(deps.arrow.core) + + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt new file mode 100644 index 0000000000..9f88733061 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -0,0 +1,30 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.tokens.repository.DefaultNetworksRepository +import com.tangem.data.tokens.repository.DefaultQuotesRepository +import com.tangem.data.tokens.repository.DefaultTokensRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokensDataModule { + + @Provides + @Singleton + fun provideTokensRepository(): TokensRepository = DefaultTokensRepository() + + @Provides + @Singleton + fun provideQuotesRepository(): QuotesRepository = DefaultQuotesRepository() + + @Provides + @Singleton + fun provideNetworksRepository(): NetworksRepository = DefaultNetworksRepository() +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt new file mode 100644 index 0000000000..dbad76f32b --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -0,0 +1,25 @@ +package com.tangem.data.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class DefaultNetworksRepository : NetworksRepository { + + override fun getNetworks(networksIds: Set): Either> { + TODO("Not yet implemented") + } + + override fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow>> { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt new file mode 100644 index 0000000000..087f265e96 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -0,0 +1,15 @@ +package com.tangem.data.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.Flow + +internal class DefaultQuotesRepository : QuotesRepository { + + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt new file mode 100644 index 0000000000..cb7575e7fa --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -0,0 +1,32 @@ +package com.tangem.data.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class DefaultTokensRepository : TokensRepository { + + override suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): Either { + TODO("Not yet implemented") + } + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> { + TODO("Not yet implemented") + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow> { + TODO("Not yet implemented") + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/domain/tokens/.gitignore b/domain/tokens/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/tokens/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts new file mode 100644 index 0000000000..d4297e1590 --- /dev/null +++ b/domain/tokens/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) + implementation(projects.core.utils) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt new file mode 100644 index 0000000000..0f52a195f9 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -0,0 +1,28 @@ +// TODO: [REDACTED_JIRA] +@file:Suppress("unused", "unused_parameter") + +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class GetTokenListUseCase( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { + return flowOf(TokensError.EmptyTokens.left()) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt new file mode 100644 index 0000000000..4647779219 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.tokens.error + +sealed class TokensError { + + object EmptyTokens : TokensError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt new file mode 100644 index 0000000000..b4b7628aa4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.tokens.model + +data class Network( + val id: ID, + val name: String, +) { + + @JvmInline + value class ID(val value: String) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt new file mode 100644 index 0000000000..ea33623175 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.tokens.model + +import arrow.core.NonEmptySet + +data class NetworkGroup( + val networkId: Network.ID, + val name: String, + val tokens: NonEmptySet, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt new file mode 100644 index 0000000000..36d1bc30e6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class NetworkStatus( + val networkId: Network.ID, + val value: Status, +) { + + sealed class Status { + open val amounts: Map? = null + } + + object Unreachable : Status() + + object MissedDerivation : Status() + + data class TransactionInProgress(override val amounts: Map) : Status() + + data class Verified(override val amounts: Map) : Status() + + data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt new file mode 100644 index 0000000000..fad3db3666 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class Quote( + val tokenId: Token.ID, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt new file mode 100644 index 0000000000..cbaa05d20e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.tokens.model + +data class Token( + val id: ID, + val networkId: Network.ID, + val name: String, + val symbol: String, + val isCustom: Boolean, + val isCoin: Boolean, +) { + + @JvmInline + value class ID(val value: String) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt new file mode 100644 index 0000000000..e94a89de44 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.tokens.model + +import arrow.core.NonEmptySet +import java.math.BigDecimal + +sealed class TokenList { + open val totalFiatBalance: FiatBalance = FiatBalance.Loading + open val sortedBy: SortType = SortType.NONE + + data class GroupedByNetwork( + val groups: NonEmptySet, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + data class Ungrouped( + val tokens: NonEmptySet, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + object NotInitialized : TokenList() + + enum class SortType { + NONE, BALANCE, + } + + sealed class FiatBalance { + object Loading : FiatBalance() + + object Failed : FiatBalance() + + data class Loaded( + val amount: BigDecimal, + val isAllAmountsSummarized: Boolean, + ) : FiatBalance() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt new file mode 100644 index 0000000000..153caf20ba --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt @@ -0,0 +1,42 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class TokenStatus( + val id: Token.ID, + val networkId: Network.ID, + val name: String, + val symbol: String, + val isCoin: Boolean, + val value: Status, +) { + + sealed class Status { + open val amount: BigDecimal? = null + open val fiatAmount: BigDecimal? = null + open val priceChange: BigDecimal? = null + open val hasTransactionsInProgress: Boolean = false + } + + object Loading : Status() + + object Unreachable : Status() + + object MissedDerivation : Status() + + object NoAccount : Status() + + data class Loaded( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal, + override val priceChange: BigDecimal, + override val hasTransactionsInProgress: Boolean, + ) : Status() + + data class Custom( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal?, + override val priceChange: BigDecimal?, + override val hasTransactionsInProgress: Boolean, + ) : Status() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt new file mode 100644 index 0000000000..df231dd283 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the blockchain networks + * */ +interface NetworksRepository { + + fun getNetworks(networksIds: Set): Either> + + fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow>> +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt new file mode 100644 index 0000000000..314c1d1177 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the quotes of tokens + * */ +interface QuotesRepository { + + fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt new file mode 100644 index 0000000000..4b1ed804c3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the tokens of user wallet + * */ +interface TokensRepository { + + suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): Either + + fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> + + fun isTokensGrouped(userWalletId: UserWalletId): Flow> + + fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 64139b1ecb..c0f88c2b7e 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -50,4 +50,5 @@ dependencies { implementation(project(":domain:models")) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.tokens) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 12a164c4fa..2406ada436 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -40,7 +40,8 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation startDestination = WalletScreens.WALLET.name, ) { composable(WalletScreens.WALLET.name) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + val viewModel = hiltViewModel() + .apply { router = this@DefaultWalletRouter } WalletScreen(state = viewModel.uiState) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 4cdda4fe30..9c80b75b31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder @@ -19,7 +20,10 @@ import kotlin.properties.Delegates [REDACTED_AUTHOR] */ @HiltViewModel -internal class WalletViewModel @Inject constructor() : ViewModel() { +internal class WalletViewModel @Inject constructor( + @Suppress("unused") // TODO: [REDACTED_JIRA] + private val getTokenListUseCase: GetTokenListUseCase, +) : ViewModel() { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() diff --git a/settings.gradle.kts b/settings.gradle.kts index 9a14394b73..423368f3d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -81,8 +81,10 @@ include(":domain:core") include(":domain:card") include(":domain:wallets") include(":domain:wallets:models") +include(":domain:tokens") // endregion Domain modules // region Data modules +include(":data:tokens") include(":data:source:preferences") // endregion Data modules \ No newline at end of file From 6d1a15724f33cd4c7480e82e78097d458bb5f320 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 11:46:29 +0300 Subject: [PATCH 026/100] Updated on 2026-08-14 --- buildSrc/build/tmp/jar/MANIFEST.MF | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 buildSrc/build/tmp/jar/MANIFEST.MF diff --git a/buildSrc/build/tmp/jar/MANIFEST.MF b/buildSrc/build/tmp/jar/MANIFEST.MF deleted file mode 100644 index 58630c02ef..0000000000 --- a/buildSrc/build/tmp/jar/MANIFEST.MF +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 - From 26f5a1da8239f4ab80759ffeb458bd410045f6d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 13:11:05 +0300 Subject: [PATCH 027/100] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 1 + .../wallet/ui/MultipleAddressUiHelper.kt | 17 +++++++---------- .../assets/configs/feature_toggles_config.json | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 36314a2e25..b1a6774b38 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -77,6 +77,7 @@ class WalletConnectSdkHelper { val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), + // TODO refactoring fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = transaction.to!!, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index 7caaabe6e1..aa62852b75 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -8,17 +8,14 @@ import com.tangem.wallet.R object MultipleAddressUiHelper { fun typeToId(type: AddressType, blockchain: Blockchain): Int { - return when (blockchain) { - in blockchainsSupportingSplit -> { - if (type == AddressType.Legacy) { - R.id.chip_legacy - } else { - R.id.chip_default - } - } - else -> { - View.NO_ID + return if (blockchain in blockchainsSupportingSplit) { + if (type == AddressType.Legacy) { + R.id.chip_legacy + } else { + R.id.chip_default } + } else { + View.NO_ID } } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index ddc2033b7b..5dc4db710d 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -1,7 +1,7 @@ [ { "name": "OPTIMISM_SWAP_FEATURE_ENABLED", - "version": "1.0.0" + "version": "4.3.1" }, { "name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED", From ffc38eb02de4d50f427d4cce9561d9ffd4f2fc58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jul 2023 22:21:20 +0800 Subject: [PATCH 028/100] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 5 +++ .../tangem/tap/di/domain/CardDomainModule.kt | 18 ++++++++++ ...ocessor.kt => DefaultScanCardProcessor.kt} | 27 ++++++++------- .../details/redux/DetailsMiddleware.kt | 5 +-- .../tap/features/home/redux/HomeMiddleware.kt | 4 +-- .../middlewares/MultiWalletMiddleware.kt | 5 +-- .../redux/WalletSelectorMiddleware.kt | 6 ++-- .../welcome/redux/WelcomeMiddleware.kt | 4 +-- .../tap/proxy/redux/DaggerGraphState.kt | 2 ++ .../tangem/domain/card/ScanCardProcessor.kt | 25 ++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 34 ++++++++++++++++--- 11 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt rename app/src/main/java/com/tangem/tap/domain/scanCard/{ScanCardProcessor.kt => DefaultScanCardProcessor.kt} (76%) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 9c3d7f67cb..b1c5862c9b 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -22,6 +22,7 @@ import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.DomainLayer +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor @@ -152,6 +153,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var tokenDetailsFeatureToggles: TokenDetailsFeatureToggles + @Inject + lateinit var scanCardProcessor: ScanCardProcessor + override fun onCreate() { super.onCreate() @@ -169,6 +173,7 @@ class TapApplication : Application(), ImageLoaderFactory { walletConnectRepository = walletConnect2Repository, walletConnectSessionsRepository = walletConnectSessionsRepository, tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, + scanCardProcessor = scanCardProcessor, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt new file mode 100644 index 0000000000..27d0e3e8df --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardDomainModule { + + @Provides + @Singleton + fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt similarity index 76% rename from app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt rename to app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 808f338f85..7c21a8a2f2 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -3,20 +3,21 @@ package com.tangem.tap.domain.scanCard import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store // TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead -internal object ScanCardProcessor { +internal class DefaultScanCardProcessor : ScanCardProcessor { private val isNewCardScanningEnabled: Boolean get() = store.state.daggerGraphState .get(DaggerGraphState::customTokenFeatureToggles) .isNewCardScanningEnabled - suspend fun scan( - cardId: String? = null, - allowsRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scan( + cardId: String?, + allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { return if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) @@ -26,15 +27,15 @@ internal object ScanCardProcessor { } @Suppress("LongParameterList") - suspend fun scan( - analyticsEvent: AnalyticsEvent? = null, - cardId: String? = null, - onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, - onWalletNotCreated: suspend () -> Unit = {}, - disclaimerWillShow: () -> Unit = {}, - onFailure: suspend (error: TangemError) -> Unit = {}, - onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + override suspend fun scan( + analyticsEvent: AnalyticsEvent?, + cardId: String?, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, + onWalletNotCreated: suspend () -> Unit, + disclaimerWillShow: () -> Unit, + onFailure: suspend (error: TangemError) -> Unit, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) { if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index a992489911..b93e243322 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -25,7 +25,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.model.builders.UserWalletIdBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.userWalletList.isLockedSync @@ -33,6 +32,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.tangemSdkManager import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers @@ -74,7 +74,8 @@ class DetailsMiddleware { is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) DetailsAction.ScanCard -> { scope.launch { - ScanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 8a225afa8a..01e2d50c0d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -21,12 +21,12 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.signin.redux.SignInAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -78,7 +78,7 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch { useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = analyticsEvent, onProgressStateChange = { showProgress -> if (showProgress) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index d008d1b2e0..3f080d645a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -15,10 +15,10 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userWalletsListManager @@ -102,7 +102,8 @@ class MultiWalletMiddleware { } private fun scanAndUpdateCard(selectedUserWallet: UserWallet) = scope.launch(Dispatchers.Default) { - ScanCardProcessor.scan(selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true) + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(cardId = selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true) .flatMap { scanResponse -> userWalletsListManager.update( userWalletId = selectedUserWallet.walletId, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 00d1e687bb..148e95d323 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -20,8 +20,8 @@ import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.model.builders.UserWalletIdBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.unlockIfLockable +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull @@ -133,7 +133,7 @@ internal class WalletSelectorMiddleware { useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), onWalletNotCreated = { // No need to rollback policy, continue with the policy set before the card scan @@ -208,7 +208,7 @@ internal class WalletSelectorMiddleware { private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult { Analytics.send(MyWallets.Button.WalletUnlockTapped()) tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse) - return ScanCardProcessor.scan() + return store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan() .map { scanResponse -> val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() if (scannedUserWalletId == userWallet.walletId) { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 8a96417455..7fab7c2a4f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -16,10 +16,10 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.signin.redux.SignInAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -103,7 +103,7 @@ internal class WelcomeMiddleware { tangemSdkManager.setAccessCodeRequestPolicy( useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn), onSuccess = { scanResponse -> scope.launch { onCardScanned(scanResponse) } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 84409d4e36..7bdabd80a3 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -2,6 +2,7 @@ package com.tangem.tap.proxy.redux import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles @@ -27,6 +28,7 @@ data class DaggerGraphState( val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsFeatureToggles: TokenDetailsFeatureToggles? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, + val scanCardProcessor: ScanCardProcessor? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt new file mode 100644 index 0000000000..961d9acd07 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.card + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.scan.ScanResponse + +interface ScanCardProcessor { + + suspend fun scan( + cardId: String? = null, + allowsRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + suspend fun scan( + analyticsEvent: AnalyticsEvent? = null, + cardId: String? = null, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, + onWalletNotCreated: suspend () -> Unit = {}, + disclaimerWillShow: () -> Unit = {}, + onFailure: suspend (error: TangemError) -> Unit = {}, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 9c80b75b31..2a2f30dcbb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -5,12 +5,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -23,6 +29,8 @@ import kotlin.properties.Delegates internal class WalletViewModel @Inject constructor( @Suppress("unused") // TODO: [REDACTED_JIRA] private val getTokenListUseCase: GetTokenListUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel() { /** Feature router */ @@ -34,16 +42,32 @@ internal class WalletViewModel @Inject constructor( // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData private fun getInitialState(): WalletStateHolder = WalletPreviewData.multicurrencyWalletScreenState.copy( - onBackClick = { router.popBackStack() }, - topBarConfig = WalletTopBarConfig( - onScanCardClick = { router.openOrganizeTokensScreen() }, - onMoreClick = { router.openDetailsScreen() }, - ), + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy( onWalletChange = ::selectWallet, ), ) + private fun onBackClick() { + router.popBackStack() + } + + private fun createTopBarConfig(): WalletTopBarConfig { + return WalletTopBarConfig( + onScanCardClick = ::onScanCardClick, + onMoreClick = { router.openDetailsScreen() }, + ) + } + + private fun onScanCardClick() { + viewModelScope.launch(dispatchers.io) { + scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { /* [REDACTED_TODO_COMMENT] */ } + .doOnFailure { /* [REDACTED_TODO_COMMENT] */ } + } + } + // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData private fun selectWallet(index: Int) { if (uiState.walletsListConfig.selectedWalletIndex == index) return From cdd80826450c879c6ed9496392950ab689315519 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Jul 2023 12:09:55 +0300 Subject: [PATCH 029/100] Updated on 2026-08-14 --- .../domain/tokens/GetTokenListUseCase.kt | 75 +++++++- .../tangem/domain/tokens/error/TokensError.kt | 15 ++ .../utils/TokenListFiatBalanceOperations.kt | 88 ++++++++++ .../tokens/utils/TokenListOperations.kt | 161 ++++++++++++++++++ .../tokens/utils/TokenStatusOperations.kt | 84 +++++++++ .../tokens/utils/TokensStatusesOperations.kt | 93 ++++++++++ 6 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 0f52a195f9..024c43ef42 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -1,19 +1,24 @@ -// TODO: [REDACTED_JIRA] -@file:Suppress("unused", "unused_parameter") - package com.tangem.domain.tokens import arrow.core.Either import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.recover +import arrow.core.right import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.tokens.utils.TokenListFiatBalanceOperations +import com.tangem.domain.tokens.utils.TokenListOperations +import com.tangem.domain.tokens.utils.TokensStatusesOperations import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flatMapConcat class GetTokenListUseCase( private val tokensRepository: TokensRepository, @@ -23,6 +28,66 @@ class GetTokenListUseCase( ) { operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { - return flowOf(TokensError.EmptyTokens.left()) + return channelFlow { + recover( + block = { + getTokenList(userWalletId, refresh).collect { list -> + send(list.right()) + } + }, + recover = { error -> + send(error.left()) + }, + ) + } + } + private fun Raise.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { + return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens -> + createTokenList(userWalletId, tokens) + } + } + + private fun Raise.getTokensStatuses( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { + val operations = TokensStatusesOperations( + tokensRepository, + quotesRepository, + networksRepository, + userWalletId, + refresh, + dispatchers, + raise = this, + ) + + return operations.getTokensStatusesFlow() + } + + private suspend fun Raise.createTokenList( + userWalletId: UserWalletId, + tokens: Set, + ): Flow { + val isAnyTokenLoading = tokens.any { it.value is TokenStatus.Loading } + val operations = TokenListOperations( + tokensRepository, + networksRepository, + userWalletId, + calculateFiatBalance(tokens, isAnyTokenLoading), + isAnyTokenLoading, + dispatchers, + raise = this, + ) + + return operations.getTokenListFlow(tokens) + } + + private suspend fun calculateFiatBalance( + tokens: Set, + isAnyTokenLoading: Boolean, + ): TokenList.FiatBalance { + val operations = TokenListFiatBalanceOperations(tokens, isAnyTokenLoading, dispatchers) + + return operations.calculateFiatBalance() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt index 4647779219..6571bc1d78 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt @@ -1,6 +1,21 @@ package com.tangem.domain.tokens.error +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.Token + sealed class TokensError { object EmptyTokens : TokensError() + + object EmptyQuotes : TokensError() + + object EmptyNetworks : TokensError() + + object EmptyNetworkStatues : TokensError() + + data class TokenAmountNotFound(val tokenId: Token.ID) : TokensError() + + data class TokenFiatAmountLessThenZero(val tokenId: Token.ID) : TokensError() + + data class NetworkNotFound(val networkId: Network.ID) : TokensError() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt new file mode 100644 index 0000000000..6d3360512e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt @@ -0,0 +1,88 @@ +package com.tangem.domain.tokens.utils + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListFiatBalanceOperations( + private val tokens: Set, + private val isAnyTokenLoading: Boolean, + private val dispatcher: CoroutineDispatcherProvider, +) { + + suspend fun calculateFiatBalance(): TokenList.FiatBalance { + return withContext(dispatcher.single) { + var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading + if (tokens.isEmpty() || isAnyTokenLoading) return@withContext fiatBalance + + for (token in tokens) { + when (val status = token.value) { + is TokenStatus.Loading -> { + fiatBalance = TokenList.FiatBalance.Loading + break + } + is TokenStatus.MissedDerivation, + is TokenStatus.Unreachable, + -> { + fiatBalance = TokenList.FiatBalance.Failed + break + } + is TokenStatus.NoAccount -> { + fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance) + } + is TokenStatus.Loaded -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + is TokenStatus.Custom -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + } + } + + fiatBalance + } + } + private fun recalculateBalanceForNoAccountStatus(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance { + return with(currentBalance) { + (this as? TokenList.FiatBalance.Loaded)?.copy( + isAllAmountsSummarized = false, + ) ?: TokenList.FiatBalance.Loaded( + amount = BigDecimal.ZERO, + isAllAmountsSummarized = false, + ) + } + } + + private fun recalculateBalance( + status: TokenStatus.Loaded, + currentBalance: TokenList.FiatBalance, + ): TokenList.FiatBalance { + return with(currentBalance) { + (this as? TokenList.FiatBalance.Loaded)?.copy( + amount = this.amount + status.fiatAmount, + ) ?: TokenList.FiatBalance.Loaded( + amount = status.fiatAmount, + isAllAmountsSummarized = true, + ) + } + } + + private fun recalculateBalance( + status: TokenStatus.Custom, + currentBalance: TokenList.FiatBalance, + ): TokenList.FiatBalance { + return with(currentBalance) { + val isTokenAmountCanBeSummarized = status.fiatAmount != null + + (this as? TokenList.FiatBalance.Loaded)?.copy( + amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO), + isAllAmountsSummarized = isTokenAmountCanBeSummarized, + ) ?: TokenList.FiatBalance.Loaded( + amount = status.fiatAmount ?: BigDecimal.ZERO, + isAllAmountsSummarized = isTokenAmountCanBeSummarized, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt new file mode 100644 index 0000000000..eeec365e6b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt @@ -0,0 +1,161 @@ +package com.tangem.domain.tokens.utils + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class TokenListOperations( + private val tokensRepository: TokensRepository, + private val networksRepository: NetworksRepository, + private val userWalletId: UserWalletId, + private val totalFiatBalance: TokenList.FiatBalance, + private val isAnyTokenLoading: Boolean, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, +) : Raise by raise { + + fun getTokenListFlow(tokens: Set): Flow { + return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> + createTokenList(tokens, isGrouped, isSortedByBalance) + } + } + + private suspend fun createTokenList( + tokens: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): TokenList = withContext(dispatchers.single) { + val tokensNes = tokens.toNonEmptySetOrNull() + + when { + tokensNes == null -> TokenList.NotInitialized + isGrouped -> { + val networks = getNetworks(tokensNes) + + createGroupedTokenList(tokensNes, networks, isSortedByBalance) + } + else -> createUngroupedTokenList(tokensNes, isSortedByBalance) + } + } + + private fun createUngroupedTokenList( + tokens: NonEmptySet, + isSortedByBalance: Boolean, + ): TokenList.Ungrouped { + return TokenList.Ungrouped( + sortedBy = getSortType(isSortedByBalance), + totalFiatBalance = totalFiatBalance, + tokens = if (isSortedByBalance) sortTokensByBalance(tokens) else tokens, + ) + } + + private fun createGroupedTokenList( + tokens: NonEmptySet, + networks: NonEmptySet, + isSortedByBalance: Boolean, + ): TokenList.GroupedByNetwork { + return TokenList.GroupedByNetwork( + sortedBy = getSortType(isSortedByBalance), + totalFiatBalance = totalFiatBalance, + groups = if (isSortedByBalance) groupAndSortTokens(tokens, networks) else groupTokens(tokens, networks), + ) + } + + private fun groupAndSortTokens( + tokens: NonEmptySet, + networks: NonEmptySet, + ): NonEmptySet { + val groupsWithSortedTokens = groupTokens(tokens, networks) + .map { group -> + group.copy(tokens = sortTokensByBalance(group.tokens)) + } + val sortedGroups = if (isAnyTokenLoading) { + groupsWithSortedTokens + } else { + sortGroupsByBalance(groupsWithSortedTokens) + } + + return sortedGroups + } + + private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { + val sortedTokens = if (isAnyTokenLoading) { + tokens + } else { + tokens + .sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } + .toNonEmptySetOrNull() + } + + return sortedTokens!! + } + + private fun groupTokens( + tokens: NonEmptySet, + networks: NonEmptySet, + ): NonEmptySet { + val groups = tokens + .groupBy { it.networkId } + .map { (networkId, tokens) -> + val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { + TokensError.NetworkNotFound(networkId) + } + + NetworkGroup( + networkId = network.id, + name = network.name, + tokens = tokens.toNonEmptySetOrNull()!!, + ) + } + .toNonEmptySetOrNull() + + return groups!! + } + + private suspend fun getNetworks(tokens: NonEmptySet): NonEmptySet { + return withContext(dispatchers.io) { + val networks = networksRepository.getNetworks(tokens.map { it.networkId }).bind() + ensureNotNull(networks.toNonEmptySetOrNull()) { TokensError.EmptyNetworks } + } + } + + private fun getIsGrouped(): Flow { + return tokensRepository.isTokensGrouped(userWalletId) + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getIsSortedByBalance(): Flow { + return tokensRepository.isTokensSortedByBalance(userWalletId) + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { + return groupsWithSortedTokens + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } + } + .toNonEmptySetOrNull()!! + } + + private fun getSortType(isSortedByBalance: Boolean) = + if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt new file mode 100644 index 0000000000..09231e0526 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt @@ -0,0 +1,84 @@ +package com.tangem.domain.tokens.utils + +import arrow.core.raise.Raise +import arrow.core.raise.ensure +import arrow.core.raise.ensureNotNull +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenStatusOperations( + private val token: Token, + private val quote: Quote?, + private val networkStatus: NetworkStatus?, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, +) : Raise by raise { + + suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.single) { + TokenStatus( + id = token.id, + networkId = token.networkId, + name = token.name, + symbol = token.symbol, + isCoin = token.isCoin, + value = createStatus(), + ) + } + + private fun createStatus(): TokenStatus.Status { + return when (val status = networkStatus?.value) { + null -> TokenStatus.Loading + is NetworkStatus.MissedDerivation -> TokenStatus.MissedDerivation + is NetworkStatus.Unreachable -> TokenStatus.Unreachable + is NetworkStatus.NoAccount -> TokenStatus.NoAccount + is NetworkStatus.TransactionInProgress, + is NetworkStatus.Verified, + -> createStatus( + amount = getTokenAmount(), + hasTransactionsInProgress = status is NetworkStatus.TransactionInProgress, + ) + } + } + + private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): TokenStatus.Status { + return when { + token.isCustom -> TokenStatus.Custom( + amount = amount, + fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), + priceChange = quote?.priceChange, + hasTransactionsInProgress = hasTransactionsInProgress, + ) + quote == null -> TokenStatus.Loading + else -> TokenStatus.Loaded( + amount = amount, + fiatAmount = calculateFiatAmount(amount, quote.fiatRate), + priceChange = quote.priceChange, + hasTransactionsInProgress = hasTransactionsInProgress, + ) + } + } + + private fun getTokenAmount(): BigDecimal { + val amount = networkStatus?.value?.amounts?.get(token.id) + return ensureNotNull(amount) { TokensError.TokenAmountNotFound(token.id) } + } + + private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? { + if (fiatRate == null) return null + + return calculateFiatAmount(amount, fiatRate) + } + + private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { + val fiatAmount = amount * fiatRate + ensure(condition = fiatAmount >= BigDecimal.ZERO) { TokensError.TokenFiatAmountLessThenZero(token.id) } + + return fiatAmount + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt new file mode 100644 index 0000000000..e301066bc6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt @@ -0,0 +1,93 @@ +package com.tangem.domain.tokens.utils + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokensStatusesOperations( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val userWalletId: UserWalletId, + private val refresh: Boolean, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, +) : Raise by raise { + + fun getTokensStatusesFlow(): Flow> { + return getTokens().flatMapConcat { + val tokens = it.toNonEmptySetOrNull() + + if (tokens == null) { + flowOf(emptySet()) + } else { + val tokensIds = tokens.map { token -> token.id } + val groupedTokens = groupTokens(tokens) + + combine(getQuotes(tokensIds), getNetworksStatues(groupedTokens)) { quotes, networksStatuses -> + createTokensStatuses(tokens, quotes, networksStatuses) + } + } + } + } + + private suspend fun createTokensStatuses( + tokens: Set, + quotes: Set, + networkStatuses: Set, + ): Set = withContext(dispatchers.single) { + tokens.map { token -> + val quote = quotes.firstOrNull { it.tokenId == token.id } + val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } + + createStatus(token, quote, networkStatus) + }.toSet() + } + + private suspend fun createStatus(token: Token, quote: Quote?, networkStatus: NetworkStatus?): TokenStatus { + val operations = TokenStatusOperations(token, quote, networkStatus, dispatchers, raise = this) + return operations.createTokenStatus() + } + + private fun getTokens(): Flow> { + return tokensRepository.getTokens(userWalletId, refresh) + .onEmpty { raise(TokensError.EmptyTokens) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow> { + return quotesRepository.getQuotes(tokensIds, refresh) + .onEmpty { raise(TokensError.EmptyQuotes) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getNetworksStatues(groupedTokens: Map>): Flow> { + return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) + .onEmpty { raise(TokensError.EmptyNetworkStatues) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private suspend fun groupTokens(tokens: NonEmptySet): Map> = + withContext(dispatchers.single) { + tokens + .groupBy { it.networkId } + .mapValues { (_, tokens) -> + tokens.toNonEmptySetOrNull() + ?.map { it.id } + ?: raise(TokensError.EmptyTokens) + } + } +} \ No newline at end of file From 121160ed8e9f6525e097a6b9be306cec52a7150b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 18:27:04 +0300 Subject: [PATCH 030/100] Updated on 2026-08-14 --- .../common/qrCodeScan/ScanQrCodeActivity.kt | 7 ++++++- .../details/ui/walletconnect/QrScanFragment.kt | 18 +++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt index f132cd319b..be7a6e4330 100644 --- a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -8,6 +8,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import com.google.zxing.BarcodeFormat import com.google.zxing.Result import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE import me.dm7.barcodescanner.zxing.ZXingScannerView @@ -16,11 +17,15 @@ import me.dm7.barcodescanner.zxing.ZXingScannerView [REDACTED_AUTHOR] */ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { + private lateinit var mScannerView: ZXingScannerView override fun onCreate(state: Bundle?) { super.onCreate(state) - mScannerView = ZXingScannerView(this) + + mScannerView = ZXingScannerView(this).apply { + setFormats(listOf(BarcodeFormat.QR_CODE)) + } setContentView(mScannerView) if (!permissionIsGranted()) requestPermission() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt index d67c7a262d..ccfe000c5c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt @@ -11,6 +11,7 @@ import androidx.activity.OnBackPressedCallback import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment +import com.google.zxing.BarcodeFormat import com.google.zxing.Result import com.otaliastudios.cameraview.CameraView import com.tangem.core.navigation.NavigationAction @@ -38,10 +39,19 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (!permissionIsGranted()) requestPermission() - scannerView = ZXingScannerView(activity) + scannerView = ZXingScannerView(activity).apply { + setFormats(listOf(BarcodeFormat.QR_CODE)) + } + return scannerView } + override fun onResume() { + super.onResume() + scannerView?.setResultHandler(this) + scannerView?.startCamera() + } + override fun onPause() { super.onPause() scannerView?.stopCamera() @@ -52,12 +62,6 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { setFitSystemWindows(fit = false) } - override fun onResume() { - super.onResume() - scannerView?.setResultHandler(this) - scannerView?.startCamera() - } - override fun handleResult(result: Result) { store.dispatch(NavigationAction.PopBackTo()) setFitSystemWindows(fit = false) From d9b36792745ef9728004c59bfec01252b9a6ebef Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Jul 2023 12:09:55 +0300 Subject: [PATCH 031/100] Updated on 2026-08-14 --- domain/tokens/build.gradle.kts | 3 + .../domain/tokens/GetTokenListUseCaseTest.kt | 334 ++++++++++++++++++ .../tangem/domain/tokens/mock/MockNetworks.kt | 90 +++++ .../tangem/domain/tokens/mock/MockQuotes.kt | 71 ++++ .../domain/tokens/mock/MockTokenLists.kt | 107 ++++++ .../tangem/domain/tokens/mock/MockTokens.kt | 90 +++++ .../domain/tokens/mock/MockTokensStates.kt | 128 +++++++ .../repository/MockNetworksRepository.kt | 27 ++ .../tokens/repository/MockQuotesRepository.kt | 16 + .../tokens/repository/MockTokensRepository.kt | 34 ++ 10 files changed, 900 insertions(+) create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index d4297e1590..d6c92daddb 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -7,4 +7,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.wallets.models) implementation(projects.core.utils) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt new file mode 100644 index 0000000000..e876c1924d --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -0,0 +1,334 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockQuotes +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.MockNetworksRepository +import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockTokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class GetTokenListUseCaseTest { + + private val dispatchers = TestingCoroutineDispatcherProvider() + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest { + // Given + val expectedResult = MockTokenLists.ungroupedTokenList.right() + + val useCase = getUseCase( + isGrouped = flowOf(false.right()), + isSortedByBalance = flowOf(false.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyTokens.left() + + val useCase = getUseCase(tokens = flowOf(expectedResult)) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyQuotes.left() + + val useCase = getUseCase(quotes = flowOf(expectedResult)) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks getting failed and list is groped then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyNetworkStatues.left() + + val useCase = getUseCase( + networks = expectedResult, + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyNetworkStatues.left() + + val useCase = getUseCase(statuses = flowOf(expectedResult)) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when grouping type getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyTokens.left() + + val useCase = getUseCase(isGrouped = flowOf(expectedResult)) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when sorting type getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokensError.EmptyTokens.left() + + val useCase = getUseCase(isSortedByBalance = flowOf(expectedResult)) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens getting failed on second emit then error should be received`() = runTest { + // Given + val error = TokensError.EmptyTokens.left() + val expectedResult = listOf( + MockTokenLists.ungroupedTokenList.right(), + error, + ) + + val useCase = getUseCase( + tokens = flowOf( + MockTokens.tokens.right(), + error, + ), + ) + + // When + val result = useCase(userWalletId) + .take(count = 2) + .toList() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list grouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.groupedTokenList.right() + + val useCase = getUseCase(isGrouped = flowOf(true.right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and networks getting failed then error should be received`() = runTest { + val expectedResult = TokensError.EmptyNetworks.left() + + val useCase = getUseCase( + networks = expectedResult, + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + isGrouped = flowOf(false.right()), + isSortedByBalance = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is sorted and grouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + isGrouped = flowOf(true.right()), + isSortedByBalance = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens is empty then not initialized token list should be received`() = runTest { + val expectedResult = MockTokenLists.notInitializedTokenList.right() + + val useCase = getUseCase(tokens = flowOf(emptySet().right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks is empty and list is grouped then error should be received`() = runTest { + val expectedResult = TokensError.EmptyNetworks.left() + + val useCase = getUseCase( + networks = emptySet().right(), + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens flow is empty then error should be received`() = runTest { + val expectedResult = TokensError.EmptyTokens.left() + + val useCase = getUseCase(tokens = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses flow is empty then error should be received`() = runTest { + val expectedResult = TokensError.EmptyNetworkStatues.left() + + val useCase = getUseCase(statuses = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses is empty then loading token list should be received`() = runTest { + val expectedResult = MockTokenLists.loadingUngroupedTokenList.right() + + val useCase = getUseCase(statuses = flowOf(emptySet().right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes flow is empty then error should be received`() = runTest { + val expectedResult = TokensError.EmptyQuotes.left() + + val useCase = getUseCase(quotes = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes is empty and statuses verified then loading token list should be received`() = runTest { + val expectedResult = MockTokenLists.loadingUngroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + quotes = flowOf(emptySet().right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase( + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + quotes: Flow>> = flowOf(MockQuotes.quotes.right()), + networks: Either> = MockNetworks.networks.right(), + statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), + isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), + isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), + ) = GetTokenListUseCase( + dispatchers = dispatchers, + tokensRepository = MockTokensRepository(tokens, isGrouped, isSortedByBalance), + quotesRepository = MockQuotesRepository(quotes), + networksRepository = MockNetworksRepository(networks, statuses), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt new file mode 100644 index 0000000000..03ad43ba70 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -0,0 +1,90 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.NonEmptySet +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworks { + + val amountToCreateAccount: BigDecimal = BigDecimal.TEN + + val network1 = Network( + id = Network.ID("network1"), + name = "Network One", + ) + + val network2 = Network( + id = Network.ID("network2"), + name = "Network Two", + ) + + val network3 = Network( + id = Network.ID("network3"), + name = "Network Three", + ) + + val networks = nonEmptySetOf(network1, network2, network3) + + val networkStatus1 = NetworkStatus( + networkId = network1.id, + value = NetworkStatus.Unreachable, + ) + + val networkStatus2 = NetworkStatus( + networkId = network2.id, + value = NetworkStatus.MissedDerivation, + ) + + val networkStatus3 = NetworkStatus( + networkId = network3.id, + value = NetworkStatus.NoAccount( + amountToCreateAccount = amountToCreateAccount, + ), + ) + + val errorNetworksStatuses = nonEmptySetOf(networkStatus1, networkStatus2, networkStatus3) + + val verifiedNetworkStatus1: NetworkStatus + get() = networkStatus1.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token1.id to BigDecimal.TEN, + MockTokens.token2.id to BigDecimal.TEN, + MockTokens.token3.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworkStatus2: NetworkStatus + get() = networkStatus2.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token4.id to BigDecimal.TEN, + MockTokens.token5.id to BigDecimal.TEN, + MockTokens.token6.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworkStatus3: NetworkStatus + get() = networkStatus3.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token7.id to BigDecimal.TEN, + MockTokens.token8.id to BigDecimal.TEN, + MockTokens.token9.id to BigDecimal.TEN, + MockTokens.token10.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworksStatuses: NonEmptySet + get() = nonEmptySetOf( + verifiedNetworkStatus1, + verifiedNetworkStatus2, + verifiedNetworkStatus3, + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt new file mode 100644 index 0000000000..767391423e --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Quote +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockQuotes { + + val quote1 = Quote( + tokenId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + tokenId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + tokenId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + tokenId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + tokenId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + tokenId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + tokenId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + tokenId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + tokenId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + tokenId = MockTokens.token10.id, + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + ) + + val quotes = nonEmptySetOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt new file mode 100644 index 0000000000..2951fbef39 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -0,0 +1,107 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokenLists { + + const val isGrouped = false + const val isSortedByBalance = false + + val networkGroup1 = NetworkGroup( + networkId = MockNetworks.network1.id, + name = MockNetworks.network1.name, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network1.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup2 = NetworkGroup( + networkId = MockNetworks.network2.id, + name = MockNetworks.network2.name, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network2.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup3 = NetworkGroup( + networkId = MockNetworks.network3.id, + name = MockNetworks.network3.name, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network3.id } + .toNonEmptySetOrNull()!!, + ) + + val networksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) + + val sortedNetworksGroups = networksGroups.map { group -> + group.copy( + tokens = MockTokensStates.loadedTokensStates + .filter { it.networkId == group.networkId } + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!!, + ) + } + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount!! } + } + .toNonEmptySetOrNull()!! + + val notInitializedTokenList = TokenList.NotInitialized + + val groupedTokenList = TokenList.GroupedByNetwork( + groups = networksGroups, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val ungroupedTokenList = TokenList.Ungrouped( + tokens = MockTokensStates.tokenStates, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val loadingUngroupedTokenList = with(ungroupedTokenList) { + copy( + tokens = tokens.map { it.copy(value = TokenStatus.Loading) }, + totalFiatBalance = TokenList.FiatBalance.Loading, + ) + } + + val sortedUngroupedTokenList: TokenList.Ungrouped + get() { + val tokens = MockTokensStates.loadedTokensStates + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!! + + return ungroupedTokenList.copy( + tokens = tokens, + sortedBy = TokenList.SortType.BALANCE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } + + val sortedGroupedTokenList: TokenList.GroupedByNetwork + get() { + val groups = sortedNetworksGroups + + return groupedTokenList.copy( + groups = groups, + sortedBy = TokenList.SortType.BALANCE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = groups + .flatMap { it.tokens } + .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt new file mode 100644 index 0000000000..e57f0f2471 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -0,0 +1,90 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Token + +internal object MockTokens { + + val token1 = Token( + id = Token.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + isCustom = false, + isCoin = true, + ) + val token2 = Token( + id = Token.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + isCoin = false, + ) + val token3 = Token( + id = Token.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + isCoin = false, + ) + val token4 = Token( + id = Token.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + isCustom = false, + isCoin = true, + ) + val token5 = Token( + id = Token.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + isCoin = true, + ) + val token6 = Token( + id = Token.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + isCoin = false, + ) + val token7 = Token( + id = Token.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + isCustom = false, + isCoin = false, + ) + val token8 = Token( + id = Token.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + isCoin = true, + ) + val token9 = Token( + id = Token.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + isCoin = true, + ) + val token10 = Token( + id = Token.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + isCoin = false, + ) + + val tokens = nonEmptySetOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt new file mode 100644 index 0000000000..338b4e1698 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -0,0 +1,128 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.TokenStatus + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokensStates { + + val tokenState1 = TokenStatus( + id = MockTokens.token1.id, + networkId = MockTokens.token1.networkId, + name = MockTokens.token1.name, + symbol = MockTokens.token1.symbol, + isCoin = MockTokens.token1.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState2 = TokenStatus( + id = MockTokens.token2.id, + networkId = MockTokens.token2.networkId, + name = MockTokens.token2.name, + symbol = MockTokens.token2.symbol, + isCoin = MockTokens.token2.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState3 = TokenStatus( + id = MockTokens.token3.id, + networkId = MockTokens.token3.networkId, + name = MockTokens.token3.name, + symbol = MockTokens.token3.symbol, + isCoin = MockTokens.token3.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState4 = TokenStatus( + id = MockTokens.token4.id, + networkId = MockTokens.token4.networkId, + name = MockTokens.token4.name, + symbol = MockTokens.token4.symbol, + isCoin = MockTokens.token4.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState5 = TokenStatus( + id = MockTokens.token5.id, + networkId = MockTokens.token5.networkId, + name = MockTokens.token5.name, + symbol = MockTokens.token5.symbol, + isCoin = MockTokens.token5.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState6 = TokenStatus( + id = MockTokens.token6.id, + networkId = MockTokens.token6.networkId, + name = MockTokens.token6.name, + symbol = MockTokens.token6.symbol, + isCoin = MockTokens.token6.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState7 = TokenStatus( + id = MockTokens.token7.id, + networkId = MockTokens.token7.networkId, + name = MockTokens.token7.name, + symbol = MockTokens.token7.symbol, + isCoin = MockTokens.token7.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState8 = TokenStatus( + id = MockTokens.token8.id, + networkId = MockTokens.token8.networkId, + name = MockTokens.token8.name, + symbol = MockTokens.token8.symbol, + isCoin = MockTokens.token8.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState9 = TokenStatus( + id = MockTokens.token9.id, + networkId = MockTokens.token9.networkId, + name = MockTokens.token9.name, + symbol = MockTokens.token9.symbol, + isCoin = MockTokens.token9.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState10 = TokenStatus( + id = MockTokens.token10.id, + networkId = MockTokens.token10.networkId, + name = MockTokens.token10.name, + symbol = MockTokens.token10.symbol, + isCoin = MockTokens.token10.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenStates = nonEmptySetOf( + tokenState1, + tokenState2, + tokenState3, + tokenState4, + tokenState5, + tokenState6, + tokenState7, + tokenState8, + tokenState9, + tokenState10, + ) + + val loadedTokensStates = tokenStates.map { state -> + val networkStatus = MockNetworks.verifiedNetworksStatuses.first { it.networkId == state.networkId } + val amount = (networkStatus.value as NetworkStatus.Verified).amounts[state.id]!! + val quote = MockQuotes.quotes.first { it.tokenId == state.id } + val fiatAmount = amount * quote.fiatRate + + state.copy( + value = TokenStatus.Loaded( + amount = amount, + fiatAmount = fiatAmount, + priceChange = quote.priceChange, + hasTransactionsInProgress = false, + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt new file mode 100644 index 0000000000..3267a30ba2 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class MockNetworksRepository( + private val networks: Either>, + private val statuses: Flow>>, +) : NetworksRepository { + + override fun getNetworks(networksIds: Set): Either> { + return networks + } + + override fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow>> { + return statuses + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt new file mode 100644 index 0000000000..e82b0c0c82 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow + +internal class MockQuotesRepository( + private val quotes: Flow>>, +) : QuotesRepository { + + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> { + return quotes + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt new file mode 100644 index 0000000000..5a7a757179 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import arrow.core.right +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class MockTokensRepository( + private val tokens: Flow>>, + private val isGrouped: Flow>, + private val isSortedByBalance: Flow>, +) : TokensRepository { + + override suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): Either = Unit.right() + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> { + return tokens + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow> { + return isGrouped + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> { + return isSortedByBalance + } +} \ No newline at end of file From c1935b59382dfbfdb7b9518f6999c85dc6054cf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 22:52:06 +0300 Subject: [PATCH 032/100] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 2 +- .../tangem/tap/features/demo/DemoHelper.kt | 4 ++-- .../redux/middlewares/RequestFeeMiddleware.kt | 2 +- .../send/redux/reducers/FeeReducer.kt | 7 +------ .../features/send/redux/states/FeeState.kt | 1 - .../wallet/ui/MultipleAddressUiHelper.kt | 19 +++++++++---------- .../wallet/ui/wallet/SingleWalletView.kt | 5 ++++- .../CurrencyExchangeManager.kt | 6 +++--- .../tap/proxy/TransactionManagerImpl.kt | 7 +++---- 9 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index b1a6774b38..6466ac2f85 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -183,7 +183,7 @@ class WalletConnectSdkHelper { val dataToSign = EthereumUtils.buildTransactionToSign( transactionData = data.transaction, nonce = null, - blockchain = data.walletManager.wallet.blockchain + blockchain = data.walletManager.wallet.blockchain, ) ?: return null val command = SignHashCommand( diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 83ea251268..ef011c6dc1 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -92,8 +92,8 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact TransactionFee.Choosable( minimum = Fee.Common(Amount(minimumFee, blockchain)), normal = Fee.Common(Amount(normalFee, blockchain)), - priority = Fee.Common(Amount(priorityFee, blockchain)) - ) + priority = Fee.Common(Amount(priorityFee, blockchain)), + ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 1b6769f0a8..efdd59f799 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -54,7 +54,7 @@ class RequestFeeMiddleware { val result = feeResult.data // val result = FeeMock.getFee(walletManager.wallet.blockchain) dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) - when(result) { + when (result) { is TransactionFee.Single -> { val fee = result.normal.amount.value ?: BigDecimal.ZERO if (fee.isZero()) { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index ba706cf281..7e2e65171e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.send.redux.reducers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.send.redux.FeeAction @@ -9,7 +8,6 @@ import com.tangem.tap.features.send.redux.SendScreenAction import com.tangem.tap.features.send.redux.states.FeeState import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.send.redux.states.TransactionExtrasState import com.tangem.tap.features.wallet.redux.ProgressState /** @@ -34,7 +32,7 @@ class FeeReducer : SendInternalReducer { } state.copy( selectedFeeType = action.feeType, - currentFee = currentFee + currentFee = currentFee, ) } is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded) @@ -43,7 +41,6 @@ class FeeReducer : SendInternalReducer { } private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState { - val result = when (action) { is FeeAction.RequestFee -> { state.copy(progressState = ProgressState.Loading) @@ -122,6 +119,4 @@ class FeeReducer : SendInternalReducer { private fun getCurrentFeeType(state: FeeState): FeeType { return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType } - - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index 1ae163571a..f7a3a5e41b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.send.redux.states -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.common.analytics.events.AnalyticsParam diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index aa62852b75..c61cb57c3f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -7,6 +7,14 @@ import com.tangem.wallet.R object MultipleAddressUiHelper { + private val blockchainsSupportingSplit = listOf( + Blockchain.Bitcoin, + Blockchain.BitcoinTestnet, + Blockchain.Litecoin, + Blockchain.BitcoinCash, + Blockchain.CardanoShelley, + ) + fun typeToId(type: AddressType, blockchain: Blockchain): Int { return if (blockchain in blockchainsSupportingSplit) { if (type == AddressType.Legacy) { @@ -22,7 +30,7 @@ object MultipleAddressUiHelper { fun idToType(id: Int, blockchain: Blockchain): AddressType? { return when (blockchain) { in blockchainsSupportingSplit -> { - when(id) { + when (id) { R.id.chip_default -> AddressType.Default R.id.chip_legacy -> AddressType.Legacy else -> null @@ -31,13 +39,4 @@ object MultipleAddressUiHelper { else -> null } } - - private val blockchainsSupportingSplit = listOf( - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.Litecoin, - Blockchain.BitcoinCash, - Blockchain.CardanoShelley - ) - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index b3ca7e7f2b..23a4645bb2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -190,7 +190,10 @@ class SingleWalletView : WalletView() { (binding.lAddress.root as? ViewGroup)?.beginDelayedTransition() chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(primaryWallet.walletAddresses.selectedAddress.type, primaryWallet.currency.blockchain) + val checkedId = MultipleAddressUiHelper.typeToId( + primaryWallet.walletAddresses.selectedAddress.type, + primaryWallet.currency.blockchain, + ) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index b04348a966..d886b5873f 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -97,9 +97,9 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( val destinationAddress = token.contractAddress val feeResult = walletManager.getFee( - amountToSend, - destinationAddress, - ) as? Result.Success ?: return + amountToSend, + destinationAddress, + ) as? Result.Success ?: return val fee = feeResult.data.minimum val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 63664407ff..fe86122123 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -226,7 +226,7 @@ class TransactionManagerImpl( ProxyFees( minFee = singleFee, normalFee = singleFee, - priorityFee = singleFee + priorityFee = singleFee, ) } is TransactionFee.Choosable -> { @@ -243,11 +243,10 @@ class TransactionManagerImpl( priorityFee = ProxyFee( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.priority.amount), - ) + ), ) } } - } is Result.Failure -> { error(fee.error.message ?: fee.error.customMessage) @@ -310,7 +309,7 @@ class TransactionManagerImpl( gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) - + ProxyFees( minFee = minProxyFee, normalFee = normalProxyFee, From 8a46d815b9fb0947f6e13213c5bd94c05dfe07f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jul 2023 08:59:18 +0500 Subject: [PATCH 033/100] Updated on 2026-08-14 --- .../marketprice/MarketPriceBlock.kt | 56 ++++++++++--------- .../marketprice/MarketPriceBlockState.kt | 17 ++++++ .../marketprice/PriceChangeConfig.kt | 9 +++ .../presentation/common/WalletPreviewData.kt | 21 ++++--- .../common/component/TokenItem.kt | 2 +- .../common/state/PriceChangeConfig.kt | 15 ----- .../common/state/TokenItemState.kt | 1 + .../state/WalletMarketplaceBlockState.kt | 33 ----------- .../wallet/state/WalletStateHolder.kt | 5 +- .../presentation/wallet/ui/WalletScreen.kt | 6 +- 10 files changed, 74 insertions(+), 91 deletions(-) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt => core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt (74%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt similarity index 74% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 7033b42c61..d7f217308c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +package com.tangem.core.ui.components.marketprice import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -17,22 +17,17 @@ 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 com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig -import com.tangem.feature.wallet.presentation.wallet.state.WalletMarketplaceBlockState /** - * Wallet marketplace block - * - * @param state state - * -[REDACTED_AUTHOR] + * @see Figma component */ @Composable -internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier: Modifier = Modifier) { +fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) { var rootWidth by remember { mutableStateOf(value = 0) } Column( modifier = modifier @@ -54,12 +49,12 @@ internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier ) when (state) { - is WalletMarketplaceBlockState.Loading -> { + is MarketPriceBlockState.Loading -> { RectangleShimmer( modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20), ) } - is WalletMarketplaceBlockState.Content -> { + is MarketPriceBlockState.Content -> { Price( config = state, priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() }, @@ -70,7 +65,7 @@ internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier } @Composable -private fun Price(config: WalletMarketplaceBlockState.Content, priceWidthDp: Dp) { +private fun Price(config: MarketPriceBlockState.Content, priceWidthDp: Dp) { Row( modifier = Modifier, verticalAlignment = Alignment.CenterVertically, @@ -124,35 +119,44 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) { @Preview @Composable -private fun Preview_MarketplaceBlock_Light( - @PreviewParameter(WalletMarketplaceStateProvider::class) - state: WalletMarketplaceBlockState, +private fun Preview_MarketPriceBlock_Light( + @PreviewParameter(WalletMarketPriceBlockStateProvider::class) + state: MarketPriceBlockState, ) { TangemTheme(isDark = false) { - WalletMarketplaceBlock(state = state) + MarketPriceBlock(state = state) } } @Preview @Composable -private fun Preview_MarketplaceBlock_Dark( - @PreviewParameter(WalletMarketplaceStateProvider::class) - state: WalletMarketplaceBlockState, +private fun Preview_MarketPriceBlock_Dark( + @PreviewParameter(WalletMarketPriceBlockStateProvider::class) + state: MarketPriceBlockState, ) { TangemTheme(isDark = true) { - WalletMarketplaceBlock(state = state) + MarketPriceBlock(state = state) } } -private class WalletMarketplaceStateProvider : CollectionPreviewParameterProvider( +private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.marketplaceBlockContent, - WalletPreviewData.marketplaceBlockContent.copy( + MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900", priceChangeConfig = PriceChangeConfig( valueInPercent = "5.16%", type = PriceChangeConfig.Type.DOWN, ), ), - WalletMarketplaceBlockState.Loading(currencyName = "BTC"), + MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "10.89%", + type = PriceChangeConfig.Type.UP, + ), + ), + MarketPriceBlockState.Loading(currencyName = "BTC"), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt new file mode 100644 index 0000000000..ae59401aa9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -0,0 +1,17 @@ +package com.tangem.core.ui.components.marketprice + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface MarketPriceBlockState { + + val currencyName: String + + data class Loading(override val currencyName: String) : MarketPriceBlockState + + data class Content( + override val currencyName: String, + val price: String, + val priceChangeConfig: PriceChangeConfig, + ) : MarketPriceBlockState +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt new file mode 100644 index 0000000000..60af2367e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.components.marketprice + +data class PriceChangeConfig(val valueInPercent: String, val type: Type) { + + /** Price changing type */ + enum class Type { + UP, DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 6f6b726f82..c89bcc47ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.R +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem @@ -204,15 +205,6 @@ internal object WalletPreviewData { WalletManageButton.CopyAddress(onClick = {}), ) - val marketplaceBlockContent = WalletMarketplaceBlockState.Content( - currencyName = "BTC", - price = "0.11$", - priceChangeConfig = PriceChangeConfig( - valueInPercent = "5.16%", - type = PriceChangeConfig.Type.UP, - ), - ) - val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent( onBackClick = {}, topBarConfig = walletTopBarConfig, @@ -300,6 +292,13 @@ internal object WalletPreviewData { ), notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), buttons = manageButtons, - marketplaceBlockState = marketplaceBlockContent, + marketPriceBlockState = MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900.12$", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "5.16%", + type = PriceChangeConfig.Type.UP, + ), + ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 62b877549a..5c4cb19fb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -28,11 +28,11 @@ import androidx.constraintlayout.compose.Dimension import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import org.burnoutcrew.reorderable.ReorderableLazyListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt deleted file mode 100644 index 9d6b4989ee..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.state - -/** - * Price changing config - * - * @property valueInPercent value in percent - * @property type type [Type] - */ -internal data class PriceChangeConfig(val valueInPercent: String, val type: Type) { - - /** Price changing type */ - enum class Type { - UP, DOWN - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 5489b5e602..b1c8417e9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.marketprice.PriceChangeConfig /** Token item state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt deleted file mode 100644 index 591c548d9e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state - -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig - -/** - * Wallet marketplace component state - * - * @property currencyName currency name - * -[REDACTED_AUTHOR] - */ -internal sealed class WalletMarketplaceBlockState(open val currencyName: String) { - - /** - * Loading state - * - * @property currencyName currency name - */ - data class Loading(override val currencyName: String) : WalletMarketplaceBlockState(currencyName = currencyName) - - /** - * Content state - * - * @property currencyName currency name - * @property price price - * @property priceChangeConfig price change config - */ - data class Content( - override val currencyName: String, - val price: String, - val priceChangeConfig: PriceChangeConfig, - ) : WalletMarketplaceBlockState(currencyName = currencyName) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index 02b9d37a3d..b9b216a198 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import kotlinx.collections.immutable.ImmutableList /** @@ -49,7 +50,7 @@ internal sealed class WalletStateHolder( * @property contentItems content items * @property notifications notifications * @property buttons manage buttons - * @property marketplaceBlockState marketplace block state + * @property marketPriceBlockState market price block state */ data class SingleCurrencyContent( override val onBackClick: () -> Unit, @@ -58,6 +59,6 @@ internal sealed class WalletStateHolder( override val contentItems: ImmutableList, override val notifications: ImmutableList, val buttons: ImmutableList, - val marketplaceBlockState: WalletMarketplaceBlockState, + val marketPriceBlockState: MarketPriceBlockState, ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 438aaf5d32..b89486d90e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.Transaction import com.tangem.core.ui.res.TangemTheme @@ -33,7 +34,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletManageButtons -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletMarketplaceBlock import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -94,8 +94,8 @@ internal fun WalletScreen(state: WalletStateHolder) { if (state is WalletStateHolder.SingleCurrencyContent) { item { - WalletMarketplaceBlock( - state = state.marketplaceBlockState, + MarketPriceBlock( + state = state.marketPriceBlockState, modifier = changeableItemModifier .padding(top = TangemTheme.dimens.spacing14) .padding(horizontal = TangemTheme.dimens.spacing16), From 7872d47eda4f698ce2e00199482eb90c8bca46be Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jul 2023 09:41:25 +0300 Subject: [PATCH 034/100] Updated on 2026-08-14 --- .../com/tangem/domain/tokens/model/Token.kt | 2 ++ .../tangem/domain/tokens/model/TokenStatus.kt | 2 ++ .../tokens/utils/TokenListOperations.kt | 4 ++- .../tokens/utils/TokenStatusOperations.kt | 2 ++ .../tokens/utils/TokensStatusesOperations.kt | 3 +- .../domain/tokens/mock/MockTokenLists.kt | 2 +- .../tangem/domain/tokens/mock/MockTokens.kt | 30 ++++++++++++++++--- .../domain/tokens/mock/MockTokensStates.kt | 20 +++++++++++++ gradle/dependencies.toml | 2 +- 9 files changed, 59 insertions(+), 8 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt index cbaa05d20e..4e1a2fba61 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt @@ -5,6 +5,8 @@ data class Token( val networkId: Network.ID, val name: String, val symbol: String, + val iconUrl: String?, + val decimals: Int, val isCustom: Boolean, val isCoin: Boolean, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt index 153caf20ba..1ea03d8aa3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt @@ -7,6 +7,8 @@ data class TokenStatus( val networkId: Network.ID, val name: String, val symbol: String, + val decimals: Int, + val iconUrl: String?, val isCoin: Boolean, val value: Status, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt index eeec365e6b..01a126b942 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt @@ -86,6 +86,7 @@ internal class TokenListOperations( .map { group -> group.copy(tokens = sortTokensByBalance(group.tokens)) } + .toNonEmptySet() val sortedGroups = if (isAnyTokenLoading) { groupsWithSortedTokens } else { @@ -131,7 +132,8 @@ internal class TokenListOperations( private suspend fun getNetworks(tokens: NonEmptySet): NonEmptySet { return withContext(dispatchers.io) { - val networks = networksRepository.getNetworks(tokens.map { it.networkId }).bind() + val networksIds = tokens.map { it.networkId }.toNonEmptySet() + val networks = networksRepository.getNetworks(networksIds).bind() ensureNotNull(networks.toNonEmptySetOrNull()) { TokensError.EmptyNetworks } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt index 09231e0526..c0ebeeafa7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt @@ -27,6 +27,8 @@ internal class TokenStatusOperations( name = token.name, symbol = token.symbol, isCoin = token.isCoin, + decimals = token.decimals, + iconUrl = token.iconUrl, value = createStatus(), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt index e301066bc6..0c90a3c80b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt @@ -31,7 +31,7 @@ internal class TokensStatusesOperations( if (tokens == null) { flowOf(emptySet()) } else { - val tokensIds = tokens.map { token -> token.id } + val tokensIds = tokens.map { token -> token.id }.toNonEmptySet() val groupedTokens = groupTokens(tokens) combine(getQuotes(tokensIds), getNetworksStatues(groupedTokens)) { quotes, networksStatuses -> @@ -87,6 +87,7 @@ internal class TokensStatusesOperations( .mapValues { (_, tokens) -> tokens.toNonEmptySetOrNull() ?.map { it.id } + ?.toNonEmptySet() ?: raise(TokensError.EmptyTokens) } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 2951fbef39..78b4810d7b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -68,7 +68,7 @@ internal object MockTokenLists { val loadingUngroupedTokenList = with(ungroupedTokenList) { copy( - tokens = tokens.map { it.copy(value = TokenStatus.Loading) }, + tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toNonEmptySet(), totalFiatBalance = TokenList.FiatBalance.Loading, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index e57f0f2471..43f22db152 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -11,7 +11,10 @@ internal object MockTokens { name = "Token 1", symbol = "T1", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = true, + ) val token2 = Token( id = Token.ID("token2"), @@ -19,6 +22,8 @@ internal object MockTokens { name = "Token 2", symbol = "T2", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = false, ) val token3 = Token( @@ -27,6 +32,8 @@ internal object MockTokens { name = "Token 3", symbol = "T3", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = false, ) val token4 = Token( @@ -35,7 +42,10 @@ internal object MockTokens { name = "Token 4", symbol = "T4", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = true, + ) val token5 = Token( id = Token.ID("token5"), @@ -43,7 +53,9 @@ internal object MockTokens { name = "Token 5", symbol = "T5", isCustom = false, - isCoin = true, + decimals = 8, + iconUrl = null, + isCoin = false, ) val token6 = Token( id = Token.ID("token6"), @@ -51,6 +63,8 @@ internal object MockTokens { name = "Token 6", symbol = "T6", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = false, ) val token7 = Token( @@ -59,7 +73,9 @@ internal object MockTokens { name = "Token 7", symbol = "T7", isCustom = false, - isCoin = false, + decimals = 8, + iconUrl = null, + isCoin = true, ) val token8 = Token( id = Token.ID("token8"), @@ -67,7 +83,9 @@ internal object MockTokens { name = "Token 8", symbol = "T8", isCustom = false, - isCoin = true, + decimals = 8, + iconUrl = null, + isCoin = false, ) val token9 = Token( id = Token.ID("token9"), @@ -75,7 +93,9 @@ internal object MockTokens { name = "Token 9", symbol = "T9", isCustom = false, - isCoin = true, + decimals = 8, + iconUrl = null, + isCoin = false, ) val token10 = Token( id = Token.ID("token10"), @@ -83,6 +103,8 @@ internal object MockTokens { name = "Token 10", symbol = "T10", isCustom = false, + decimals = 8, + iconUrl = null, isCoin = false, ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 338b4e1698..0b0c9e41ab 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -12,6 +12,8 @@ internal object MockTokensStates { networkId = MockTokens.token1.networkId, name = MockTokens.token1.name, symbol = MockTokens.token1.symbol, + decimals = MockTokens.token1.decimals, + iconUrl = MockTokens.token1.iconUrl, isCoin = MockTokens.token1.isCoin, value = TokenStatus.Unreachable, ) @@ -21,6 +23,8 @@ internal object MockTokensStates { networkId = MockTokens.token2.networkId, name = MockTokens.token2.name, symbol = MockTokens.token2.symbol, + decimals = MockTokens.token2.decimals, + iconUrl = MockTokens.token2.iconUrl, isCoin = MockTokens.token2.isCoin, value = TokenStatus.Unreachable, ) @@ -30,6 +34,8 @@ internal object MockTokensStates { networkId = MockTokens.token3.networkId, name = MockTokens.token3.name, symbol = MockTokens.token3.symbol, + decimals = MockTokens.token3.decimals, + iconUrl = MockTokens.token3.iconUrl, isCoin = MockTokens.token3.isCoin, value = TokenStatus.Unreachable, ) @@ -39,6 +45,8 @@ internal object MockTokensStates { networkId = MockTokens.token4.networkId, name = MockTokens.token4.name, symbol = MockTokens.token4.symbol, + decimals = MockTokens.token4.decimals, + iconUrl = MockTokens.token4.iconUrl, isCoin = MockTokens.token4.isCoin, value = TokenStatus.MissedDerivation, ) @@ -48,6 +56,8 @@ internal object MockTokensStates { networkId = MockTokens.token5.networkId, name = MockTokens.token5.name, symbol = MockTokens.token5.symbol, + decimals = MockTokens.token5.decimals, + iconUrl = MockTokens.token5.iconUrl, isCoin = MockTokens.token5.isCoin, value = TokenStatus.MissedDerivation, ) @@ -57,6 +67,8 @@ internal object MockTokensStates { networkId = MockTokens.token6.networkId, name = MockTokens.token6.name, symbol = MockTokens.token6.symbol, + decimals = MockTokens.token6.decimals, + iconUrl = MockTokens.token6.iconUrl, isCoin = MockTokens.token6.isCoin, value = TokenStatus.MissedDerivation, ) @@ -66,6 +78,8 @@ internal object MockTokensStates { networkId = MockTokens.token7.networkId, name = MockTokens.token7.name, symbol = MockTokens.token7.symbol, + decimals = MockTokens.token7.decimals, + iconUrl = MockTokens.token7.iconUrl, isCoin = MockTokens.token7.isCoin, value = TokenStatus.NoAccount, ) @@ -75,6 +89,8 @@ internal object MockTokensStates { networkId = MockTokens.token8.networkId, name = MockTokens.token8.name, symbol = MockTokens.token8.symbol, + decimals = MockTokens.token8.decimals, + iconUrl = MockTokens.token8.iconUrl, isCoin = MockTokens.token8.isCoin, value = TokenStatus.NoAccount, ) @@ -84,6 +100,8 @@ internal object MockTokensStates { networkId = MockTokens.token9.networkId, name = MockTokens.token9.name, symbol = MockTokens.token9.symbol, + decimals = MockTokens.token9.decimals, + iconUrl = MockTokens.token9.iconUrl, isCoin = MockTokens.token9.isCoin, value = TokenStatus.NoAccount, ) @@ -93,6 +111,8 @@ internal object MockTokensStates { networkId = MockTokens.token10.networkId, name = MockTokens.token10.name, symbol = MockTokens.token10.symbol, + decimals = MockTokens.token10.decimals, + iconUrl = MockTokens.token10.iconUrl, isCoin = MockTokens.token10.isCoin, value = TokenStatus.NoAccount, ) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e7fc8fd730..9b0fbad374 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -71,7 +71,7 @@ zxingQrBarcodeScanner = "1.9.8" zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" -arrow = "1.2.0-RC" +arrow = "1.2.0" reactiveNetwork = "3.0.8" walletConnectCore = "1.17.0" walletConnectWeb3 = "1.10.0" From 2672b38f1258fe896515f575258c78c50d84b996 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 23:20:44 +0300 Subject: [PATCH 035/100] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TapApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index b1c5862c9b..00e2ae0a86 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory +import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat @@ -179,6 +180,7 @@ class TapApplication : Application(), ImageLoaderFactory { ) if (BuildConfig.DEBUG) { + Logger.addLogAdapter(AndroidLogAdapter()) Timber.plant( object : Timber.DebugTree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { From 8e44c110bb2a4ba8bba9e01b03d5c105dcde1bee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jul 2023 09:44:52 +0300 Subject: [PATCH 036/100] Updated on 2026-08-14 --- .../tangem/data/tokens/di/TokensDataModule.kt | 12 +- .../tangem/data/tokens/mock/MockNetworks.kt | 56 ++++++++ .../com/tangem/data/tokens/mock/MockQuotes.kt | 70 ++++++++++ .../com/tangem/data/tokens/mock/MockTokens.kt | 132 ++++++++++++++++++ ...epository.kt => MockNetworksRepository.kt} | 17 ++- ...sRepository.kt => MockQuotesRepository.kt} | 12 +- ...sRepository.kt => MockTokensRepository.kt} | 16 ++- 7 files changed, 297 insertions(+), 18 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt rename data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/{DefaultNetworksRepository.kt => MockNetworksRepository.kt} (60%) rename data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/{DefaultQuotesRepository.kt => MockQuotesRepository.kt} (56%) rename data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/{DefaultTokensRepository.kt => MockTokensRepository.kt} (60%) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 9f88733061..85b6f06d93 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -1,8 +1,8 @@ package com.tangem.data.tokens.di -import com.tangem.data.tokens.repository.DefaultNetworksRepository -import com.tangem.data.tokens.repository.DefaultQuotesRepository -import com.tangem.data.tokens.repository.DefaultTokensRepository +import com.tangem.data.tokens.repository.MockNetworksRepository +import com.tangem.data.tokens.repository.MockQuotesRepository +import com.tangem.data.tokens.repository.MockTokensRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository @@ -18,13 +18,13 @@ internal object TokensDataModule { @Provides @Singleton - fun provideTokensRepository(): TokensRepository = DefaultTokensRepository() + fun provideTokensRepository(): TokensRepository = MockTokensRepository() @Provides @Singleton - fun provideQuotesRepository(): QuotesRepository = DefaultQuotesRepository() + fun provideQuotesRepository(): QuotesRepository = MockQuotesRepository() @Provides @Singleton - fun provideNetworksRepository(): NetworksRepository = DefaultNetworksRepository() + fun provideNetworksRepository(): NetworksRepository = MockNetworksRepository() } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt new file mode 100644 index 0000000000..af85a6322b --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt @@ -0,0 +1,56 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworks { + + val network1 = Network( + id = Network.ID("network1"), + name = "Network One", + ) + + val network2 = Network( + id = Network.ID("network2"), + name = "Network Two", + ) + + val network3 = Network( + id = Network.ID("network3"), + name = "Network Three", + ) + + val networks = setOf(network1, network2, network3) + + val networkStatus1 = NetworkStatus( + networkId = network1.id, + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token1.id to BigDecimal("123.1234556789"), + MockTokens.token2.id to BigDecimal("42.2"), + MockTokens.token3.id to BigDecimal("1000000000.5"), + ), + ), + ) + + val networkStatus2 = NetworkStatus( + networkId = network2.id, + value = NetworkStatus.MissedDerivation, + ) + + val networkStatus3 = NetworkStatus( + networkId = network3.id, + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token7.id to BigDecimal.ZERO, + MockTokens.token8.id to BigDecimal.TEN, + MockTokens.token9.id to BigDecimal.TEN, + MockTokens.token10.id to BigDecimal.TEN, + ), + ), + ) + + val networksStatuses = setOf(networkStatus1, networkStatus2, networkStatus3) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt new file mode 100644 index 0000000000..86f7a6d76c --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt @@ -0,0 +1,70 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Quote +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockQuotes { + + val quote1 = Quote( + tokenId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + tokenId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + tokenId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + tokenId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + tokenId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + tokenId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + tokenId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + tokenId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + tokenId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + tokenId = MockTokens.token10.id, + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + ) + + val quotes = setOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt new file mode 100644 index 0000000000..4261727cff --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -0,0 +1,132 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId + +internal object MockTokens { + + val token1 get() = Token( + id = Token.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token2 get() = Token( + id = Token.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token3 get() = Token( + id = Token.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token4 get() = Token( + id = Token.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token5 get() = Token( + id = Token.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token6 get() = Token( + id = Token.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token7 get() = Token( + id = Token.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token8 get() = Token( + id = Token.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token9 get() = Token( + id = Token.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token10 get() = Token( + id = Token.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + + val tokens get() = mapOf( + UserWalletId(stringValue = "123") to setOf( + token1, token2, token3, token4, token5, + token6, token7, token8, token9, token10, + ), + UserWalletId(stringValue = "321") to setOf(token1, token2, token3), + UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), + UserWalletId(stringValue = "24") to setOf(token4, token5, token6), + ) + + val isGrouped get() = mapOf( + UserWalletId(stringValue = "123") to true, + UserWalletId(stringValue = "321") to false, + UserWalletId(stringValue = "42") to false, + UserWalletId(stringValue = "24") to true, + ) + + val isSortedByBalance get() = mapOf( + UserWalletId(stringValue = "123") to true, + UserWalletId(stringValue = "321") to false, + UserWalletId(stringValue = "42") to true, + UserWalletId(stringValue = "24") to false, + ) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt similarity index 60% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt index dbad76f32b..a328f0a544 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt @@ -1,6 +1,8 @@ package com.tangem.data.tokens.repository import arrow.core.Either +import arrow.core.right +import com.tangem.data.tokens.mock.MockNetworks import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -8,11 +10,15 @@ import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -internal class DefaultNetworksRepository : NetworksRepository { +internal class MockNetworksRepository : NetworksRepository { override fun getNetworks(networksIds: Set): Either> { - TODO("Not yet implemented") + return MockNetworks.networks + .filter { it.id in networksIds } + .toSet() + .right() } override fun getNetworkStatuses( @@ -20,6 +26,11 @@ internal class DefaultNetworksRepository : NetworksRepository { networks: Map>, refresh: Boolean, ): Flow>> { - TODO("Not yet implemented") + return flowOf( + MockNetworks.networksStatuses + .filter { it.networkId in networks.keys } + .toSet() + .right(), + ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt similarity index 56% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt index 087f265e96..1e6cbaa1d8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt @@ -1,15 +1,23 @@ package com.tangem.data.tokens.repository import arrow.core.Either +import arrow.core.right +import com.tangem.data.tokens.mock.MockQuotes import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.QuotesRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -internal class DefaultQuotesRepository : QuotesRepository { +internal class MockQuotesRepository : QuotesRepository { override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> { - TODO("Not yet implemented") + return flowOf( + MockQuotes.quotes + .filter { it.tokenId in tokensIds } + .toSet() + .right(), + ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt similarity index 60% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt index cb7575e7fa..92092f1046 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt @@ -1,32 +1,34 @@ package com.tangem.data.tokens.repository import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.data.tokens.mock.MockTokens import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -internal class DefaultTokensRepository : TokensRepository { +internal class MockTokensRepository : TokensRepository { override suspend fun sortTokens( userWalletId: UserWalletId, sortedTokensIds: Set, isGrouped: Boolean, isSortedByBalance: Boolean, - ): Either { - TODO("Not yet implemented") - } + ): Either = Unit.right() override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> { - TODO("Not yet implemented") + return flowOf(value = MockTokens.tokens[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) } override fun isTokensGrouped(userWalletId: UserWalletId): Flow> { - TODO("Not yet implemented") + return flowOf(MockTokens.isGrouped[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) } override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> { - TODO("Not yet implemented") + return flowOf(MockTokens.isSortedByBalance[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) } } \ No newline at end of file From d192fe840582674fdb6f545489733f03af0fad8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 08:46:16 +0300 Subject: [PATCH 037/100] Updated on 2026-08-14 --- .../wallet2/ui/components/OnboardingDescriptionBlock.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt index 27a76c7f38..4134269aaa 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource @@ -62,9 +63,13 @@ fun DescriptionTitleText(text: String) { @Composable fun DescriptionSubTitleText(text: String) { + // the text style made similar to TextViewOnboarding.Body Text( text = text, - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.body1.copy( + lineHeight = 20.sp, + letterSpacing = 0.03.sp, + ), color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), From 0e1caf7c0370ec1560dafcd8552514d9cda35c58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Jul 2023 15:21:34 +0300 Subject: [PATCH 038/100] Updated on 2026-08-14 --- .../onboarding/api/OnboardingSeedPhrase.kt | 1 - .../domain/DefaultSeedPhraseInteractor.kt | 2 +- .../wallet2/viewmodel/SeedPhraseErrorConverter.kt | 2 +- .../wallet2/viewmodel/SeedPhraseViewModel.kt | 15 +++------------ 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt index 2db7541c5a..2f6b1d4a0c 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt @@ -54,7 +54,6 @@ private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState state = uiState.introState, ) } - SeedPhraseScreen.AboutSeedPhrase -> { AboutSeedPhraseScreen( state = uiState.aboutState, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt index d0cd349b55..f3152c931e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt @@ -96,7 +96,7 @@ internal class DefaultSeedPhraseInteractor constructor( } private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) { - MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidEntropyLength + MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt index dbb553ba63..9a44793b6f 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt @@ -14,7 +14,7 @@ class SeedPhraseErrorConverter : ModuleMessageConverter { + SeedPhraseError.InvalidChecksum -> { context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum) } is SeedPhraseError.InvalidWords -> { diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index b5d2b6a8d7..2e6e7587eb 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -25,7 +25,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject /** @@ -67,7 +66,6 @@ class SeedPhraseViewModel @Inject constructor( ) private val textFieldsDebouncers = mutableMapOf() - private var suggestionWordInserted: AtomicBoolean = AtomicBoolean(false) private var generatedMnemonicComponents: List? = null private var importedMnemonicComponents: List? = null @@ -197,19 +195,14 @@ class SeedPhraseViewModel @Inject constructor( val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) when { - suggestionWordInserted.getAndSet(false) -> { - debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { - validateMnemonic(inputMnemonic) - } - } - isSameText && !isCursorMoved -> { - // do nothing - } isSameText && isCursorMoved -> { debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { updateSuggestions(fieldState) } } + isSameText && !isCursorMoved -> { + // do nothing + } else -> { debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { updateSuggestions(fieldState) @@ -243,7 +236,6 @@ class SeedPhraseViewModel @Inject constructor( is SeedPhraseError.InvalidWords -> { uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words) } - else -> uiState } updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) } @@ -366,7 +358,6 @@ class SeedPhraseViewModel @Inject constructor( private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { viewModelScope.launchSingle { - suggestionWordInserted.set(true) val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] val cursorPosition = textFieldValue.selection.end From bde99e272b94fdce814567fd29cc2a711607c9e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jul 2023 16:28:43 +0800 Subject: [PATCH 039/100] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +- .../main/java/com/tangem/tap/MainActivity.kt | 66 ++++++++++--------- .../AccessCodeRequestPolicyMiddleware.kt | 8 +-- .../java/com/tangem/tap/di/ActivityModule.kt | 41 ++++++------ .../com/tangem/tap/domain/TangemSdkManager.kt | 66 +++++-------------- .../walletconnect/WalletConnectSdkHelper.kt | 7 +- .../details/redux/DetailsMiddleware.kt | 12 ++-- .../tap/features/home/redux/HomeMiddleware.kt | 9 ++- .../redux/OnboardingWalletMiddleware.kt | 14 ++-- .../saveWallet/redux/SaveWalletMiddleware.kt | 8 ++- .../send/redux/middlewares/SendMiddleware.kt | 4 +- .../middlewares/TradeCryptoMiddleware.kt | 2 +- .../redux/WalletSelectorMiddleware.kt | 12 ++-- .../welcome/redux/WelcomeMiddleware.kt | 4 +- .../CurrencyExchangeManager.kt | 10 +-- .../com/tangem/tap/proxy/AppStateHolder.kt | 2 - .../tap/proxy/TransactionManagerImpl.kt | 5 +- .../com/tangem/tap/proxy/di/ProxyModule.kt | 8 ++- .../tap/proxy/redux/DaggerGraphAction.kt | 2 + .../tap/proxy/redux/DaggerGraphReducer.kt | 1 + .../tap/proxy/redux/DaggerGraphState.kt | 2 + data/card/.gitignore | 1 + data/card/build.gradle.kts | 21 ++++++ .../card/DefaultCardSdkConfigRepository.kt | 53 +++++++++++++++ .../com/tangem/data/card/di/CardDataModule.kt | 21 ++++++ .../com/tangem/data/card/di/CardSdkModule.kt | 23 +++++++ .../data/card/sdk/CardSdkLifecycleObserver.kt | 17 +++++ .../tangem/data/card/sdk/CardSdkProvider.kt | 14 ++++ .../data/card/sdk/DefaultCardSdkProvider.kt | 45 +++++++++++++ .../com/tangem/domain/card/ScanCardUseCase.kt | 58 +++++----------- .../repository/CardSdkConfigRepository.kt | 28 ++++++++ settings.gradle.kts | 1 + 32 files changed, 384 insertions(+), 184 deletions(-) create mode 100644 data/card/.gitignore create mode 100644 data/card/build.gradle.kts create mode 100644 data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07f7cd8b81..75db227f9d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(project(":domain:legacy")) implementation(project(":domain:models")) implementation(project(":domain:core")) - implementation(project(":domain:card")) + implementation(projects.domain.card) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) implementation(projects.domain.tokens) @@ -41,6 +41,7 @@ dependencies { implementation(project(":libs:crypto")) implementation(project(":libs:auth")) implementation(project(":data:source:preferences")) + implementation(projects.data.card) implementation(projects.data.tokens) /** Features */ diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2eb8371482..1c4395d748 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -10,10 +10,11 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar -import com.tangem.TangemSdk import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.data.card.sdk.CardSdkLifecycleObserver import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -56,7 +57,6 @@ import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext -lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService internal var lockUserWalletsTimer: LockUserWalletsTimer? = null @@ -88,7 +88,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lateinit var testerRouter: TesterRouter @Inject - lateinit var injectedTangemSdk: TangemSdk + lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver + + @Inject + lateinit var cardSdkConfigRepository: CardSdkConfigRepository @Inject lateinit var injectedTangemSdkManager: TangemSdkManager @@ -122,11 +125,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac systemActions() store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) - tangemSdk = injectedTangemSdk + cardSdkLifecycleObserver.onCreate(context = this) + tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager - appStateHolder.tangemSdk = tangemSdk - backupService = BackupService.init(tangemSdk, this) + backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer(owner = this) initUserWalletsListManager() @@ -144,10 +147,37 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, + cardSdkConfigRepository = cardSdkConfigRepository, ), ) } + override fun onStart() { + super.onStart() + dialogManager.onStart(this) + } + + override fun onResume() { + super.onResume() + // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop + notificationsHandler = NotificationsHandler(binding.fragmentContainer) + + navigateToInitialScreenIfNeededOnResume(intent) + } + + override fun onStop() { + notificationsHandler = null + dialogManager.onStop() + super.onStop() + } + + override fun onDestroy() { + store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) + intentProcessor.removeAll() + cardSdkLifecycleObserver.onDestroy() + super.onDestroy() + } + private fun initIntentHandlers() { val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider)) @@ -183,13 +213,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } - override fun onResume() { - super.onResume() - notificationsHandler = NotificationsHandler(binding.fragmentContainer) - - navigateToInitialScreenIfNeededOnResume(intent) - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) scope.launch { @@ -197,23 +220,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - override fun onStart() { - super.onStart() - dialogManager.onStart(this) - } - - override fun onStop() { - notificationsHandler = null - dialogManager.onStop() - super.onStop() - } - - override fun onDestroy() { - store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) - intentProcessor.removeAll() - super.onDestroy() - } - override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) { if (snackbar != null) return diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt index a96f1c1e06..c385b121ae 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -3,7 +3,8 @@ package com.tangem.tap.common.redux import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.preferencesStorage -import com.tangem.tap.tangemSdkManager +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import org.rekotlin.Middleware class AccessCodeRequestPolicyMiddleware { @@ -19,9 +20,8 @@ class AccessCodeRequestPolicyMiddleware { } private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - scanResponse.card.isAccessCodeSet, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 96639f64e4..a894d7c4fe 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,42 +1,43 @@ package com.tangem.tap.di import android.content.Context -import androidx.fragment.app.FragmentActivity -import com.tangem.TangemSdk import com.tangem.domain.card.ScanCardUseCase -import com.tangem.sdk.extensions.initWithBiometrics +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.userTokensRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object ActivityModule { @Provides - @ActivityScoped - fun provideTangemSdk(@ActivityContext context: Context): TangemSdk { - return TangemSdk.initWithBiometrics(context as FragmentActivity, TangemSdkManager.config) + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context, + cardSdkConfigRepository: CardSdkConfigRepository, + ): TangemSdkManager { + return TangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) } @Provides - @ActivityScoped - fun provideTangemSdkManager(@ActivityContext context: Context, tangemSdk: TangemSdk): TangemSdkManager { - return TangemSdkManager(tangemSdk, context) - } - - @Provides - @ActivityScoped - fun provideScanCardUseCase(tangemSdk: TangemSdk, tangemSdkManager: TangemSdkManager): ScanCardUseCase { + @Singleton + fun provideScanCardUseCase( + tangemSdkManager: TangemSdkManager, + cardSdkConfigRepository: CardSdkConfigRepository, + ): ScanCardUseCase { return ScanCardUseCase( - tangemSdk = tangemSdk, - scanCardRepository = DefaultScanCardRepository(userTokensRepository, tangemSdkManager), + cardSdkConfigRepository = cardSdkConfigRepository, + scanCardRepository = DefaultScanCardRepository( + userTokensRepository = userTokensRepository, + tangemSdkManager = tangemSdkManager, + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index c395569c5c..2ac23842a6 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,27 +1,25 @@ package com.tangem.tap.domain -import android.content.Context +import android.content.res.Resources import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk import com.tangem.blockchain.common.Blockchain import com.tangem.common.* import com.tangem.common.biometric.BiometricManager -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask -import com.tangem.operations.pins.CheckUserCodesCommand -import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.tap.common.analytics.events.Basic @@ -38,7 +36,10 @@ import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions") -class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { +class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources) { + + private val tangemSdk: TangemSdk + get() = cardSdkConfigRepository.sdk private val userCodeRepository by lazy { UserCodeRepository( @@ -63,7 +64,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { - val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) + val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, @@ -80,7 +81,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsync( CreateProductWalletTask(scanResponse.cardTypesResolver), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)), + Message(resources.getString(R.string.initial_message_create_wallet_body)), ) } @@ -92,7 +93,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co is CompletionResult.Success -> runTaskAsync( CreateProductWalletTask(scanResponse.cardTypesResolver, seedResult.data), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)), + Message(resources.getString(R.string.initial_message_create_wallet_body)), ) is CompletionResult.Failure -> CompletionResult.Failure(seedResult.error) @@ -111,7 +112,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( CreateWalletAndRescanTask(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), + initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)), ) .map { CardDTO(it) } } @@ -127,7 +128,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( runnable = ResetToFactorySettingsTask(), cardId = cardId, - initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)), + initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), ) .map { CardDTO(it) } } @@ -154,7 +155,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)), + initialMessage = Message(resources.getString(R.string.initial_message_change_passcode_body)), ) } @@ -162,7 +163,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)), + initialMessage = Message(resources.getString(R.string.initial_message_change_access_code_body)), ) } @@ -170,15 +171,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), - ) - } - - suspend fun checkUserCodes(cardId: String?): CompletionResult { - return runTaskAsyncReturnOnMain( - CheckUserCodesCommand(), - cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) } @@ -186,7 +179,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeRecoveryAllowedTask(enabled), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) } @@ -197,7 +190,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( runnable = ScanTask(allowRequestAccessCodeFromRepository), cardId = cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) .map { CardDTO(it) } } @@ -233,31 +226,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } } + @Deprecated("TangemSdkManager shouldn't returns a string from resources") fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { - return context.getString(stringResId, *formatArgs) - } - - fun setAccessCodeRequestPolicy(useBiometricsForAccessCode: Boolean) { - tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) { - UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) - } else { - UserCodeRequestPolicy.Default - } - } - - fun useBiometricsForAccessCode(): Boolean { - val policy = tangemSdk.config.userCodeRequestPolicy - return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode - } - - companion object { - val config = Config( - linkedTerminal = true, - allowUntrustedCards = true, - filter = CardFilter( - allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), - maxFirmwareVersion = FirmwareVersion(major = 4, minor = 52), - ), - ) + return resources.getString(stringResId, *formatArgs) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 6466ac2f85..03f8cf7d40 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -36,8 +36,8 @@ import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTrans import com.tangem.tap.features.details.redux.walletconnect.* import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdkManager import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.* import timber.log.Timber @@ -163,7 +163,10 @@ class WalletConnectSdkHelper { private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? { val result = (data.walletManager as TransactionSender).send( transactionData = data.transaction, - signer = CommonSigner(tangemSdk, cardId), + signer = CommonSigner( + tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, + cardId = cardId, + ), ) return when (result) { SimpleResult.Success -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index b93e243322..e665b7cee3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -372,9 +372,9 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldSaveAccessCodes = true - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = scanResponse?.card?.isAccessCodeSet == true, - ) + store.state.daggerGraphState + .get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true) return CompletionResult.Success(Unit) } @@ -385,9 +385,9 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off)) preferencesStorage.shouldSaveAccessCodes = false - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = false, - ) + store.state.daggerGraphState + .get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false) } .doOnFailure { error -> Timber.e(error, "Unable to delete saved access codes") diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 01e2d50c0d..547440e96f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -26,7 +25,11 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.signin.redux.SignInAction +import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -74,8 +77,8 @@ private fun handleHomeAction(action: Action) { private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch { delay(timeMillis = 200) - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 67921cce64..fe5800266a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -30,9 +30,9 @@ import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.wallet.models.toCurrencies import com.tangem.tap.features.wallet.redux.Artwork +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Action -import org.rekotlin.DispatchFunction import org.rekotlin.Middleware object OnboardingWalletMiddleware { @@ -43,8 +43,8 @@ private val onboardingWalletMiddleware: Middleware = { dispatch, state { next -> { action -> when (action) { - is OnboardingWallet2Action -> handleWallet2Action(action, state) - else -> handleWalletAction(action, state, dispatch) + is OnboardingWallet2Action -> handleWallet2Action(action) + else -> handleWalletAction(action) } next(action) } @@ -52,7 +52,7 @@ private val onboardingWalletMiddleware: Middleware = { dispatch, state } @Suppress("LongMethod", "ComplexMethod") -private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) { +private fun handleWalletAction(action: Action) { if (action !is OnboardingWalletAction) return val globalState = store.state.globalState @@ -178,7 +178,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } @Suppress("LongMethod", "ComplexMethod") -private fun handleWallet2Action(action: OnboardingWallet2Action, state: () -> AppState?) { +private fun handleWallet2Action(action: OnboardingWallet2Action) { val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager @@ -329,7 +329,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) is BackupAction.AddBackupCard -> { backupService.addBackupCard { result -> backupService.skipCompatibilityChecks = false - tangemSdk.config.filter.cardIdFilter = null + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk + .config.filter.cardIdFilter = null + when (result) { is CompletionResult.Success -> { store.dispatchOnMain(BackupAction.AddBackupCard.Success) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 945fc90afd..370e759dff 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -22,6 +22,7 @@ import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.isLockable import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -110,9 +111,10 @@ internal class SaveWalletMiddleware { // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { preferencesStorage.shouldSaveAccessCodes = true - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = userWallet.hasAccessCode, - ) + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) } val savedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 69891195d6..a76724c7c3 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -7,7 +7,6 @@ import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras -import com.tangem.core.navigation.NavigationAction import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult @@ -15,6 +14,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics +import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO @@ -39,6 +39,7 @@ import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -201,6 +202,7 @@ private fun sendTransaction( return@launch } + val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk val linkedTerminalState = tangemSdk.config.linkedTerminal if (card.isStart2Coin) { tangemSdk.config.linkedTerminal = false diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index fbb76f391a..65be537ac8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -72,7 +72,7 @@ class TradeCryptoMiddleware { } scope.launch { - exchangeManager.buyErc20TestnetTokens( + buyErc20TestnetTokens( card = card, walletManager = walletManager, token = currency.token, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 148e95d323..a331a52719 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -126,11 +126,13 @@ internal class WalletSelectorMiddleware { private fun addWallet() = scope.launch { Analytics.send(MyWallets.Button.ScanNewCard()) - val prevUseBiometricsForAccessCode = tangemSdkManager.useBiometricsForAccessCode() + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() // Update access code policy for access code saving when a card was scanned - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( @@ -147,14 +149,14 @@ internal class WalletSelectorMiddleware { saveUserWalletAndPopBackToWalletScreen(scanResponse) .doOnFailure { error -> // Rollback policy if card saving was failed - tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to save user wallet") store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) } }, onFailure = { error -> // Rollback policy if card scanning was failed - tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to scan card") store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) }, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 7fab7c2a4f..c10e1bcad2 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -100,8 +100,8 @@ internal class WelcomeMiddleware { } private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn), diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index d886b5873f..ae3e244978 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -13,8 +13,8 @@ import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import com.tangem.tap.tangemSdk import java.math.BigDecimal /** @@ -86,11 +86,7 @@ class CurrencyExchangeManager( } } -suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( - card: CardDTO, - walletManager: EthereumWalletManager, - token: Token, -) { +suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) @@ -109,7 +105,7 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( val signer = TangemSigner( card = card, - tangemSdk = tangemSdk, + tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, initialMessage = Message(), ) { signResponse -> store.dispatch( diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cf6015d446..7533a00f58 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy -import com.tangem.TangemSdk import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder import com.tangem.domain.models.scan.CardDTO @@ -30,7 +29,6 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState var userTokensRepository: UserTokensRepository? = null var mainStore: Store? = null var tangemSdkManager: TangemSdkManager? = null - var tangemSdk: TangemSdk? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index fe86122123..7c3e6f8b1e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -14,6 +14,7 @@ import com.tangem.blockchain.extensions.isNetworkError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.TransactionManager @@ -24,7 +25,6 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.tangemSdk import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -34,6 +34,7 @@ import java.math.RoundingMode class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val analytics: AnalyticsEventHandler, + private val cardSdkConfigRepository: CardSdkConfigRepository, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -395,7 +396,7 @@ class TransactionManagerImpl( val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" } return TangemSigner( card = actualCard, - tangemSdk = tangemSdk, + tangemSdk = cardSdkConfigRepository.sdk, initialMessage = Message(), ) { signResponse -> appStateHolder.mainStore?.dispatch( diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 371f1e21bf..cc2e04969d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.proxy.di import androidx.compose.ui.text.intl.Locale import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager @@ -39,8 +40,13 @@ class ProxyModule { fun provideTransactionManager( appStateHolder: AppStateHolder, analytics: AnalyticsEventHandler, + cardSdkConfigRepository: CardSdkConfigRepository, ): TransactionManager { - return TransactionManagerImpl(appStateHolder, analytics) + return TransactionManagerImpl( + appStateHolder = appStateHolder, + analytics = analytics, + cardSdkConfigRepository = cardSdkConfigRepository, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index e83ed33f3a..1b0f19c9fe 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,6 +1,7 @@ package com.tangem.tap.proxy.redux import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -15,5 +16,6 @@ sealed interface DaggerGraphAction : Action { val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, val tokenDetailsRouter: TokenDetailsRouter, + val cardSdkConfigRepository: CardSdkConfigRepository, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index bdd590be4d..aec162a122 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -18,6 +18,7 @@ object DaggerGraphReducer { walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, tokenDetailsRouter = action.tokenDetailsRouter, + cardSdkConfigRepository = action.cardSdkConfigRepository, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 7bdabd80a3..228d3f014b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -29,6 +30,7 @@ data class DaggerGraphState( val tokenDetailsFeatureToggles: TokenDetailsFeatureToggles? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, + val cardSdkConfigRepository: CardSdkConfigRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/data/card/.gitignore b/data/card/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/card/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts new file mode 100644 index 0000000000..69bbb44e78 --- /dev/null +++ b/data/card/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.card" +} + +dependencies { + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + implementation(deps.tangem.card.android) + implementation(deps.tangem.card.core) + + implementation(projects.domain.card) + implementation(projects.domain.models) +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt new file mode 100644 index 0000000000..e263616c2f --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -0,0 +1,53 @@ +package com.tangem.data.card + +import com.tangem.TangemSdk +import com.tangem.common.UserCodeType +import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.scan.ProductType + +/** + * Implementation of repository for managing with CardSDK config + * + * @property cardSdkProvider CardSDK instance provider + * +[REDACTED_AUTHOR] + */ +internal class DefaultCardSdkConfigRepository( + private val cardSdkProvider: CardSdkProvider, +) : CardSdkConfigRepository { + + @Deprecated("Use CardSdkConfigRepository's methods instead of this property") + override val sdk: TangemSdk + get() = cardSdkProvider.sdk + + override fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) { + sdk.config.userCodeRequestPolicy = if (isBiometricsRequestPolicy) { + UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) + } else { + UserCodeRequestPolicy.Default + } + } + + override fun isBiometricsRequestPolicy(): Boolean { + return with(sdk.config.userCodeRequestPolicy) { + this is UserCodeRequestPolicy.AlwaysWithBiometrics && codeType == UserCodeType.AccessCode + } + } + + override fun resetCardIdDisplayFormat() { + sdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full + } + + override fun updateCardIdDisplayFormat(productType: ProductType) { + sdk.config.cardIdDisplayFormat = when (productType) { + ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4) + ProductType.Note, + ProductType.Wallet, + ProductType.Start2Coin, + -> CardIdDisplayFormat.Full + } + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt new file mode 100644 index 0000000000..4ed7adf268 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -0,0 +1,21 @@ +package com.tangem.data.card.di + +import com.tangem.data.card.DefaultCardSdkConfigRepository +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.domain.card.repository.CardSdkConfigRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardDataModule { + + @Provides + @Singleton + fun provideCardSdkConfigRepository(cardSdkProvider: CardSdkProvider): CardSdkConfigRepository { + return DefaultCardSdkConfigRepository(cardSdkProvider = cardSdkProvider) + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt new file mode 100644 index 0000000000..c88f2dbfc2 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt @@ -0,0 +1,23 @@ +package com.tangem.data.card.di + +import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.card.sdk.DefaultCardSdkProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CardSdkModule { + + @Binds + @Singleton + fun provideCardSdkProvider(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkProvider + + @Binds + @Singleton + fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt new file mode 100644 index 0000000000..f5f6781392 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt @@ -0,0 +1,17 @@ +package com.tangem.data.card.sdk + +import android.content.Context + +/** + * Lifecycle observer for creating Card SDK instance + * +[REDACTED_AUTHOR] + */ +interface CardSdkLifecycleObserver { + + /** Callback of creating activity [context] */ + fun onCreate(context: Context) + + /** Callback of destroying activity */ + fun onDestroy() +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt new file mode 100644 index 0000000000..0d0b88fa41 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.data.card.sdk + +import com.tangem.TangemSdk + +/** + * CardSDK instance provider + * +[REDACTED_AUTHOR] + */ +internal interface CardSdkProvider { + + /** CardSDK instance */ + val sdk: TangemSdk +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt new file mode 100644 index 0000000000..dddff7e3da --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -0,0 +1,45 @@ +package com.tangem.data.card.sdk + +import android.content.Context +import androidx.fragment.app.FragmentActivity +import com.tangem.TangemSdk +import com.tangem.common.CardFilter +import com.tangem.common.card.FirmwareVersion +import com.tangem.common.core.Config +import com.tangem.sdk.extensions.initWithBiometrics +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of CardSDK instance provider + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver { + + override val sdk: TangemSdk + get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" } + + private var _sdk: TangemSdk? = null + + override fun onCreate(context: Context) { + _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + } + + override fun onDestroy() { + _sdk = null + } + + private companion object { + + val config = Config( + linkedTerminal = true, + allowUntrustedCards = true, + filter = CardFilter( + allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), + maxFirmwareVersion = FirmwareVersion(major = 4, minor = 52), + ), + ) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 46312c439f..69dbc7a617 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -4,47 +4,46 @@ import arrow.core.Either import arrow.core.EitherNel import arrow.core.flatMap import com.tangem.TangemSdk -import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.ChainProcessor -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse /** - * Use case responsible for scanning a card and returning a [ScanResponse] object. - * @property scanCardRepository A repository object implementing [ScanCardRepository] interface. - * @property tangemSdk An instance of [TangemSdk] to configure the display format of the card ID. - * @constructor Create a new instance of [ScanCardUseCase] with the given dependencies. + * Use case responsible for scanning a card and returning a [ScanResponse] object + * + * @property scanCardRepository a repository object implementing [ScanCardRepository] interface + * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID + * + * @constructor create a new instance of [ScanCardUseCase] with the given dependencies */ class ScanCardUseCase( private val scanCardRepository: ScanCardRepository, - private val tangemSdk: TangemSdk, + private val cardSdkConfigRepository: CardSdkConfigRepository, ) { - /** - * A [ChainProcessor] object to launch the after-scan chains. - */ + /** A [ChainProcessor] object to launch the after-scan chains */ private val scanChainProcessor by lazy { ChainProcessor() } /** * Scan a card and return a [ScanResponse] object. - * @param cardId an optional card ID to scan. If null, can scan any card present. - * Defaults to null. - * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed. - * Defaults to false. - * @param afterScanChains A list of chains that should be executed after a successful card scan operation. - * Defaults to an empty array. - * @return A [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse]. + * + * @param cardId an optional card ID to scan. If null, can scan any card present + * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed + * @param afterScanChains a list of chains that should be executed after a successful card scan + * operation. Defaults to an empty array + * + * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse] */ suspend operator fun invoke( cardId: String? = null, allowRequestAccessCodeFromStorage: Boolean = false, afterScanChains: List> = emptyList(), ): Either { - resetCardIdDisplayFormat() + cardSdkConfigRepository.resetCardIdDisplayFormat() scanChainProcessor.addChains(afterScanChains) return scanCardRepository.scanCard( @@ -52,31 +51,10 @@ class ScanCardUseCase( allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, ) .onRight { scanResponse -> - updateCardIdDisplayFormat(scanResponse.productType) + cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) } .flatMap { response -> scanChainProcessor.launchChains(initial = response) } } - - /** - * Reset the card ID display format to [CardIdDisplayFormat.Full]. - */ - private fun resetCardIdDisplayFormat() { - tangemSdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full - } - - /** - * Update the card ID display format according to the [ProductType] of the scanned card. - * @param productType The [ProductType] of the scanned card. - */ - private fun updateCardIdDisplayFormat(productType: ProductType) { - tangemSdk.config.cardIdDisplayFormat = when (productType) { - ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4) - ProductType.Note, - ProductType.Wallet, - ProductType.Start2Coin, - -> CardIdDisplayFormat.Full - } - } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt new file mode 100644 index 0000000000..11fa7e26e1 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.card.repository + +import com.tangem.TangemSdk +import com.tangem.domain.models.scan.ProductType + +/** + * Repository for managing with CardSDK config + * +[REDACTED_AUTHOR] + */ +interface CardSdkConfigRepository { + + /** Tangem SDK instance */ + @Deprecated("Use CardSdkConfigRepository's methods instead of this property") + val sdk: TangemSdk + + /** Set access code request policy by [isBiometricsRequestPolicy] */ + fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) + + /** Check if config has biometrics request policy */ + fun isBiometricsRequestPolicy(): Boolean + + /** Reset the card ID display format to start value */ + fun resetCardIdDisplayFormat() + + /** Update the card ID display format according to the [productType] of the scanned card */ + fun updateCardIdDisplayFormat(productType: ProductType) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 423368f3d8..4cd9f9833e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,6 +85,7 @@ include(":domain:tokens") // endregion Domain modules // region Data modules +include(":data:card") include(":data:tokens") include(":data:source:preferences") // endregion Data modules \ No newline at end of file From d8348e54df0cbcc2e5cb7d8fa68c7c648bf4a5bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jul 2023 09:50:38 +0300 Subject: [PATCH 040/100] Updated on 2026-08-14 --- .../core/ui/utils/BigDecimalFormatter.kt | 71 ++++++++++++ .../domain/tokens/model/NetworkGroup.kt | 4 +- .../tangem/domain/tokens/model/TokenList.kt | 5 +- .../tokens/utils/TokenListOperations.kt | 2 +- .../domain/tokens/mock/MockTokenLists.kt | 4 +- features/wallet/impl/build.gradle.kts | 1 + .../presentation/common/WalletPreviewData.kt | 15 ++- .../utils/FiatBalanceToWalletCardConverter.kt | 40 +++++++ .../wallet/utils/LoadingItemsProvider.kt | 15 +++ .../utils/TokenErrorToWalletStateConverter.kt | 15 +++ .../utils/TokenListToContentItemsConverter.kt | 63 +++++++++++ .../utils/TokenListToWalletStateConverter.kt | 56 ++++++++++ .../utils/TokenStatusToTokenItemConverter.kt | 105 ++++++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 79 +++++++++++-- 14 files changed, 448 insertions(+), 27 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt new file mode 100644 index 0000000000..d10357cd54 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -0,0 +1,71 @@ +package com.tangem.core.ui.utils + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Currency +import java.util.Locale + +object BigDecimalFormatter { + + private const val TEMP_CURRENCY_CODE = "USD" + + fun formatCryptoAmount( + cryptoAmount: BigDecimal, + cryptoCurrency: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { + val formatterCurrency = getCurrency(cryptoCurrency) + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + roundingMode = RoundingMode.DOWN + } + + return formatter.format(cryptoAmount) + .replace(formatterCurrency.getSymbol(locale), cryptoCurrency) + } + + fun formatFiatAmount( + fiatAmount: BigDecimal, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + val formatterCurrency = getCurrency(fiatCurrencyCode) + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + + return formatter.format(fiatAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + + fun formatPercent(percent: BigDecimal, useAbsoluteValue: Boolean, locale: Locale = Locale.getDefault()): String { + val formatter = NumberFormat.getPercentInstance(locale).apply { + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + val value = if (useAbsoluteValue) percent.abs() else percent + + return formatter.format(value) + } + + private fun getCurrency(code: String): Currency { + return runCatching { Currency.getInstance(code) } + .getOrElse { e -> + // Currency code is not valid ISO 4217 code + if (e is IllegalArgumentException) { + Currency.getInstance(TEMP_CURRENCY_CODE) + } else { + throw e + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index ea33623175..acc05ff374 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -1,9 +1,7 @@ package com.tangem.domain.tokens.model -import arrow.core.NonEmptySet - data class NetworkGroup( val networkId: Network.ID, val name: String, - val tokens: NonEmptySet, + val tokens: Set, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index e94a89de44..3f1ffaaf75 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -1,6 +1,5 @@ package com.tangem.domain.tokens.model -import arrow.core.NonEmptySet import java.math.BigDecimal sealed class TokenList { @@ -8,13 +7,13 @@ sealed class TokenList { open val sortedBy: SortType = SortType.NONE data class GroupedByNetwork( - val groups: NonEmptySet, + val groups: Set, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() data class Ungrouped( - val tokens: NonEmptySet, + val tokens: Set, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt index 01a126b942..f5d8bf0d5c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt @@ -84,7 +84,7 @@ internal class TokenListOperations( ): NonEmptySet { val groupsWithSortedTokens = groupTokens(tokens, networks) .map { group -> - group.copy(tokens = sortTokensByBalance(group.tokens)) + group.copy(tokens = sortTokensByBalance(group.tokens as NonEmptySet)) } .toNonEmptySet() val sortedGroups = if (isAnyTokenLoading) { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 78b4810d7b..c705c614f2 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -68,7 +68,7 @@ internal object MockTokenLists { val loadingUngroupedTokenList = with(ungroupedTokenList) { copy( - tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toNonEmptySet(), + tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toSet(), totalFiatBalance = TokenList.FiatBalance.Loading, ) } @@ -91,7 +91,7 @@ internal object MockTokenLists { val sortedGroupedTokenList: TokenList.GroupedByNetwork get() { - val groups = sortedNetworksGroups + val groups = sortedNetworksGroups.toSet() return groupedTokenList.copy( groups = groups, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index c0f88c2b7e..c216409615 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) + implementation(deps.arrow.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index c89bcc47ee..dab4813f30 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem @@ -51,14 +52,16 @@ internal object WalletPreviewData { onClick = null, ) + val wallets = mapOf( + UserWalletId(stringValue = "123") to walletCardContentState, + UserWalletId(stringValue = "321") to walletCardLoadingState, + UserWalletId(stringValue = "42") to walletCardHiddenContentState, + UserWalletId(stringValue = "24") to walletCardErrorState, + ) + val walletListConfig = WalletsListConfig( selectedWalletIndex = 0, - wallets = persistentListOf( - walletCardContentState, - walletCardLoadingState, - walletCardHiddenContentState, - walletCardErrorState, - ), + wallets = wallets.values.toPersistentList(), onWalletChange = {}, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt new file mode 100644 index 0000000000..2fe69a783c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState +import com.tangem.utils.converter.Converter + +internal class FiatBalanceToWalletCardConverter( + private val currentState: WalletCardState, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + override fun convert(value: TokenList.FiatBalance): WalletCardState { + // TODO: [REDACTED_JIRA] + return when (value) { + is TokenList.FiatBalance.Loading -> with(currentState) { + WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) + } + is TokenList.FiatBalance.Failed -> with(currentState) { + WalletCardState.Error(id, title, additionalInfo, imageResId, onClick) + } + is TokenList.FiatBalance.Loaded -> with(currentState) { + if (isWalletContentHidden) { + WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick) + } else { + WalletCardState.Content( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onClick = onClick, + balance = formatFiatAmount(value.amount, fiatCurrencyCode, fiatCurrencySymbol), + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt new file mode 100644 index 0000000000..c00f864c32 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal object LoadingItemsProvider { + + fun getLoadingMultiCurrencyTokens(): PersistentList { + return List(size = 5) { TokenItemState.Loading } + .map { MultiCurrencyItem.Token(it) } + .toPersistentList() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt new file mode 100644 index 0000000000..0f8cb0ca2f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.error.TokensError +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.utils.converter.Converter + +internal class TokenErrorToWalletStateConverter( + private val currentState: WalletStateHolder, +) : Converter { + + // TODO: [REDACTED_JIRA] + override fun convert(value: TokensError): WalletStateHolder { + return currentState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt new file mode 100644 index 0000000000..731c766675 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem +import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf + +internal class TokenListToContentItemsConverter( + isWalletContentHidden: Boolean, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, +) : Converter> { + + private val tokenStatusConverter = TokenStatusToTokenItemConverter( + isWalletContentHidden, + fiatCurrencyCode, + fiatCurrencySymbol, + ) + + override fun convert(value: TokenList): ImmutableList { + return when (value) { + is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() + is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() + is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() + } + } + + private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList { + return groups.fold(initial = persistentListOf()) { acc, group -> + acc.mutate { it.addGroup(group) } + } + } + + private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList { + return tokens.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + this.add(MultiCurrencyItem.NetworkGroupTitle(group.name)) + + group.tokens.forEach { token -> + this.addToken(token) + } + + return this + } + + private fun MutableList.addToken(token: TokenStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + + this.add(MultiCurrencyItem.Token(tokenItemState)) + + return this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt new file mode 100644 index 0000000000..af7cf9373e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.SingleCurrencyContent +import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListToWalletStateConverter( + private val currentState: WalletStateHolder, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + override fun convert(value: TokenList): WalletStateHolder { + return when (currentState) { + is MultiCurrencyContent -> currentState.updateWithTokenList(value) + is SingleCurrencyContent -> currentState.updateWithTokenList(value) + } + } + + private fun MultiCurrencyContent.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { + val converter = TokenListToContentItemsConverter(isWalletContentHidden, fiatCurrencyCode, fiatCurrencySymbol) + + return this.copy( + walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), + contentItems = converter.convert(tokenList), + ) + } + + private fun SingleCurrencyContent.updateWithTokenList(tokenList: TokenList): SingleCurrencyContent { + return this.copy( + walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), + ) + } + + private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { + val selectedWalletIndex = walletsListConfig.selectedWalletIndex + val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] + val converter = FiatBalanceToWalletCardConverter( + selectedWalletCard, + isWalletContentHidden, + fiatCurrencyCode, + fiatCurrencySymbol, + ) + + return walletsListConfig.copy( + wallets = walletsListConfig.wallets + .toPersistentList() + .set(selectedWalletIndex, converter.convert(fiatBalance)), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt new file mode 100644 index 0000000000..b654b47713 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt @@ -0,0 +1,105 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenStatusToTokenItemConverter( + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + private val TokenStatus.networkIconResId: Int? + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return if (isCoin) null else R.drawable.img_eth_22 + } + + private val TokenStatus.tokenIconResId: Int + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return R.drawable.img_eth_22 + } + + override fun convert(value: TokenStatus): TokenItemState { + return when (value.value) { + is TokenStatus.Loading -> TokenItemState.Loading + is TokenStatus.Loaded, + is TokenStatus.Custom, + -> value.mapToTokenItemState() + // TODO: Add other token item states, currently not designed + is TokenStatus.MissedDerivation, + is TokenStatus.NoAccount, + is TokenStatus.Unreachable, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun TokenStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = this.id.value, + name = this.name, + tokenIconUrl = this.iconUrl, + tokenIconResId = this.tokenIconResId, + networkIconResId = this.networkIconResId, + amount = getFormattedAmount(), + hasPending = value.hasTransactionsInProgress, + tokenOptions = if (isWalletContentHidden) { + TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig()) + } else { + TokenItemState.TokenOptionsState.Visible( + fiatAmount = getFormattedFiatAmount(), + priceChange = getPriceChangeConfig(), + ) + }, + ) + } + + private fun TokenStatus.getFormattedAmount(): String { + val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, symbol, decimals) + } + + private fun TokenStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol) + } + + private fun TokenStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = this.id.value, + name = this.name, + tokenIconUrl = this.iconUrl, + tokenIconResId = this.tokenIconResId, + networkIconResId = this.networkIconResId, + ) + + private fun TokenStatus.getPriceChangeConfig(): PriceChangeConfig { + val priceChange = value.priceChange + ?: return PriceChangeConfig(UNKNOWN_AMOUNT_SIGN, PriceChangeConfig.Type.DOWN) + + return PriceChangeConfig( + valueInPercent = BigDecimalFormatter.formatPercent(priceChange, useAbsoluteValue = true), + type = priceChange.getPriceChangeType(), + ) + } + + private fun BigDecimal?.getPriceChangeType(): PriceChangeConfig.Type { + return when { + this == null -> PriceChangeConfig.Type.DOWN + this < BigDecimal.ZERO -> PriceChangeConfig.Type.DOWN + else -> PriceChangeConfig.Type.UP + } + } + + private companion object { + const val UNKNOWN_AMOUNT_SIGN = "—" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 2a2f30dcbb..9ee5a8a430 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -6,16 +6,26 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.Either import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.feature.wallet.presentation.wallet.utils.TokenErrorToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -27,7 +37,6 @@ import kotlin.properties.Delegates */ @HiltViewModel internal class WalletViewModel @Inject constructor( - @Suppress("unused") // TODO: [REDACTED_JIRA] private val getTokenListUseCase: GetTokenListUseCase, private val scanCardProcessor: ScanCardProcessor, private val dispatchers: CoroutineDispatcherProvider, @@ -40,14 +49,29 @@ internal class WalletViewModel @Inject constructor( var uiState by mutableStateOf(getInitialState()) private set + private var getTokenListJob: Job? = null + set(value) { + field?.cancel() + field = value + } + // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun getInitialState(): WalletStateHolder = WalletPreviewData.multicurrencyWalletScreenState.copy( - onBackClick = ::onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy( - onWalletChange = ::selectWallet, - ), - ) + private fun getInitialState(): WalletStateHolder { + val state = WalletPreviewData.multicurrencyWalletScreenState.copy( + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy( + onWalletChange = ::selectWallet, + ), + contentItems = getLoadingMultiCurrencyTokens(), + ) + + val selectedWalletIndex = state.walletsListConfig.selectedWalletIndex + val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(selectedWalletIndex) + launchGetTokenListJob(selectedWalletId) + + return state + } private fun onBackClick() { router.popBackStack() @@ -74,14 +98,45 @@ internal class WalletViewModel @Inject constructor( Log.i("WalletViewModel", "selectWallet: $index") - uiState = if (index % 2 == 0) { - WalletPreviewData.multicurrencyWalletScreenState.copy( + uiState = when (val state = uiState) { + is WalletStateHolder.MultiCurrencyContent -> state.copy( walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), ) - } else { - WalletPreviewData.singleWalletScreenState.copy( + is WalletStateHolder.SingleCurrencyContent -> state.copy( walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), ) } + + val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(index) + launchGetTokenListJob(selectedWalletId) + } + + private fun launchGetTokenListJob(userWalletId: UserWalletId) { + getTokenListJob = getTokenListUseCase(userWalletId) + .distinctUntilChanged() + .mapLatest(::updateStateWithTokenListOrError) + .onEach { uiState = it } + .flowOn(Dispatchers.Default) + .launchIn(viewModelScope) + } + + private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { + val updateStateWithError = { error: TokensError -> + val converter = TokenErrorToWalletStateConverter(uiState) + + converter.convert(error) + } + val updateState = { list: TokenList -> + val converter = TokenListToWalletStateConverter( + uiState, + isWalletContentHidden = false, // TODO: [REDACTED_JIRA] + fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] + fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + ) + + converter.convert(list) + } + + return tokenList.fold(ifLeft = updateStateWithError, ifRight = updateState) } } \ No newline at end of file From de899b5678cbeee965f3f9d0ebf11ff1b9d13262 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jul 2023 11:37:10 +0300 Subject: [PATCH 041/100] Updated on 2026-08-14 --- .../com/tangem/tap/common/chat/ChatManager.kt | 8 +++- .../tap/common/chat/opener/ChatOpener.kt | 3 +- .../implementation/SprinklrChatOpener.kt | 5 +- .../implementation/ZendeskChatOpener.kt | 32 +++++++++---- .../tap/common/feedback/FeedbackManager.kt | 48 +++++++++++++++---- 5 files changed, 73 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt index 182aa7350b..56ded693ad 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt @@ -12,6 +12,7 @@ import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener import com.tangem.tap.common.redux.AppState import org.rekotlin.Store +import java.io.File class ChatManager( private val preferencesStorage: PreferencesDataSource, @@ -20,7 +21,7 @@ class ChatManager( ) { private val openers = mutableMapOf() - fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) { + fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) { val opener = openers.getOrPut(config) { when (config) { is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver) @@ -28,7 +29,10 @@ class ChatManager( } } - opener.open(feedbackDataBuilder) + opener.open( + createFeedbackFile = createFeedbackFile, + createLogsFile = createLogsFile, + ) } private fun getZendeskUserId(): String { diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt index e3757286e7..aff21cad05 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt @@ -1,7 +1,8 @@ package com.tangem.tap.common.chat.opener import android.content.Context +import java.io.File internal interface ChatOpener { - fun open(feedbackDataBuilder: (Context) -> String) + fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt index c9b8fdb4f9..f8936d6abd 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt @@ -2,14 +2,15 @@ package com.tangem.tap.common.chat.opener.implementation import android.content.Context import android.content.Intent +import com.tangem.datasource.config.models.SprinklrConfig import com.tangem.tap.ForegroundActivityObserver import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.datasource.config.models.SprinklrConfig import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.sprinklr.redux.SprinklrAction import com.tangem.tap.features.sprinklr.ui.SprinklrActivity import com.tangem.tap.withForegroundActivity import org.rekotlin.Store +import java.io.File internal class SprinklrChatOpener( private val userId: String, @@ -17,7 +18,7 @@ internal class SprinklrChatOpener( private val store: Store, private val foregroundActivityObserver: ForegroundActivityObserver, ) : ChatOpener { - override fun open(feedbackDataBuilder: (Context) -> String) { + override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { store.dispatch(SprinklrAction.Init(userId, config)) foregroundActivityObserver.withForegroundActivity { activity -> val intent = Intent(activity, SprinklrActivity::class.java) diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt index 0e5d38b0da..1c6198806c 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt @@ -2,34 +2,34 @@ package com.tangem.tap.common.chat.opener.implementation import android.content.Context import com.tangem.core.analytics.Analytics +import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.domain.common.LogConfig import com.tangem.tap.ForegroundActivityObserver import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R import com.zendesk.logger.Logger -import zendesk.chat.Chat -import zendesk.chat.ChatConfiguration -import zendesk.chat.ChatEngine -import zendesk.chat.ChatProvidersConfiguration -import zendesk.chat.VisitorInfo +import timber.log.Timber +import zendesk.chat.* import zendesk.configurations.Configuration import zendesk.messaging.MessagingActivity +import java.io.File internal class ZendeskChatOpener( private val userId: String, private val config: ZendeskConfig, private val foregroundActivityObserver: ForegroundActivityObserver, ) : ChatOpener { + private var isInitialized = false - override fun open(feedbackDataBuilder: (Context) -> String) { + override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { foregroundActivityObserver.withForegroundActivity { activity -> initZendeskIfNeeded(activity.applicationContext) setChatVisitorInfo() - setChatVisitorNote(feedbackDataBuilder(activity)) showMessagingActivity(activity) + sendFeedbackFile(createFeedbackFile(activity)) + sendLogsFile(createLogsFile(activity)) } } @@ -48,8 +48,20 @@ internal class ZendeskChatOpener( ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build() } - private fun setChatVisitorNote(note: String) { - Chat.INSTANCE.providers()?.profileProvider()?.setVisitorNote(note) + private fun sendFeedbackFile(feedbackFile: File?) { + if (isInitialized && feedbackFile != null) { + Chat.INSTANCE.providers()?.chatProvider()?.sendFile(feedbackFile) { _, bytesUploaded, _ -> + Timber.d("Log file sent", "bytesUploaded: $bytesUploaded") + } + } + } + + private fun sendLogsFile(logsFile: File?) { + if (isInitialized && logsFile != null) { + Chat.INSTANCE.providers()?.chatProvider()?.sendFile(logsFile) { _, bytesUploaded, _ -> + Timber.d("Log file sent", "bytesUploaded: $bytesUploaded") + } + } } private fun showMessagingActivity(context: Context) { diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 6fddf99607..c1e9ae43b1 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,9 +1,9 @@ package com.tangem.tap.common.feedback import android.content.Context +import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds import com.tangem.tap.common.chat.ChatManager -import com.tangem.datasource.config.models.ChatConfig import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver @@ -22,32 +22,61 @@ class FeedbackManager( private val chatManager: ChatManager, ) { + private var sessionFeedbackFile: File? = null + private var sessionLogsFile: File? = null + fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { feedbackData.prepare(infoHolder) foregroundActivityObserver.withForegroundActivity { activity -> - val fileLog = createLogFile(activity) activity.sendEmail( email = getSupportEmail(), subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), - file = fileLog, + file = getLogFile(activity), onFail = onFail, ) } } fun openChat(config: ChatConfig, feedbackData: FeedbackData) { - chatManager.open(config) { context -> - feedbackData.run { + chatManager.open( + config = config, + createLogsFile = ::getLogFile, + createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) }, + ) + } + + private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? { + return try { + if (sessionFeedbackFile != null) { + return sessionFeedbackFile + } + val file = File(context.filesDir, FEEDBACK_FILE) + file.delete() + file.createNewFile() + + val feedback = feedbackData.run { prepare(infoHolder) joinTogether(context, infoHolder) } + val fileWriter = FileWriter(file) + fileWriter.write(feedback) + fileWriter.close() + + sessionFeedbackFile = file + sessionFeedbackFile + } catch (ex: Exception) { + Timber.e(ex, "Can't create the logs file") + null } } - private fun createLogFile(context: Context): File? { + private fun getLogFile(context: Context): File? { return try { - val file = File(context.filesDir, "logs.txt") + if (sessionLogsFile != null) { + return sessionLogsFile + } + val file = File(context.filesDir, LOGS_FILE) file.delete() file.createNewFile() @@ -57,7 +86,8 @@ class FeedbackManager( fileWriter.write(stringWriter.toString()) fileWriter.close() logCollector.clearLogs() - file + sessionLogsFile = file + sessionLogsFile } catch (ex: Exception) { Timber.e(ex, "Can't create the logs file") null @@ -75,5 +105,7 @@ class FeedbackManager( companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" + const val FEEDBACK_FILE = "feedback.txt" + const val LOGS_FILE = "logs.txt" } } \ No newline at end of file From 1a73ce8390a17e0f4e2223f2f6f5b0f188b78d08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jul 2023 13:05:56 +0300 Subject: [PATCH 042/100] Updated on 2026-08-14 --- .../tangem/tap/common/extensions/Specific.kt | 32 ++++++++++++++++--- .../tap/features/wallet/ui/BalanceWidget.kt | 8 ++--- .../wallet/ui/WalletDetailsFragment.kt | 6 ++-- .../wallet/ui/adapters/WalletAdapter.kt | 4 +-- .../wallet/ui/utils/WalletDataOperations.kt | 6 ++-- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index bea9728880..5f259977e0 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -24,13 +24,17 @@ fun BigDecimal.toFormattedString( df.decimalFormatSymbols = symbols df.maximumFractionDigits = decimals df.minimumFractionDigits = 0 - df.isGroupingUsed = false + df.isGroupingUsed = true df.roundingMode = roundingMode return df.format(this) } +/** + * To formatted crypto currency string + * Specific method because there is no crypto currency codes in Locale + */ @Suppress("MagicNumber") -fun BigDecimal.toFormattedCurrencyString( +fun BigDecimal.toFormattedCryptoCurrencyString( decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN, @@ -41,16 +45,34 @@ fun BigDecimal.toFormattedCurrencyString( } else { decimals } + try { + val locale = Locale.getDefault() + val formatter = NumberFormat.getCurrencyInstance(locale) + // first create currency instance for "USD" with Locale default to replace currency to crypto later + Currency.getInstance("USD")?.let { currencyTmp -> + formatter.currency = currencyTmp + formatter.maximumFractionDigits = decimalsForRounding + formatter.minimumFractionDigits = 0 + formatter.isGroupingUsed = true + formatter.roundingMode = roundingMode + // cause formatter created for USD, replace Currency with Crypto symbol on right by Locale place + return formatter.format(this).replace(currencyTmp.getSymbol(locale), "$currency ") + } + } catch (e: IllegalArgumentException) { + Timber.e(e, "can't parse currency") + } + // if something went wrong - use old way to format val formattedAmount = this.toFormattedString( decimals = decimalsForRounding, roundingMode = roundingMode, + locale = Locale.getDefault(), ) - return "$formattedAmount $currency" + return "$formattedAmount $currency " } fun BigDecimal.toFiatRateString(fiatCurrencyName: String, fiatCode: String): String { try { - val formatter = NumberFormat.getCurrencyInstance() + val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault()) Currency.getInstance(fiatCode)?.let { currency -> formatter.currency = currency formatter.maximumFractionDigits = 2 @@ -91,7 +113,7 @@ fun BigDecimal.toFormattedFiatValue( formatWithSpaces: Boolean = false, ): String { try { - val formatter = NumberFormat.getCurrencyInstance() + val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault()) Currency.getInstance(fiatCode)?.let { currency -> formatter.currency = currency formatter.maximumFractionDigits = 2 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 23aa7d9d6e..3cab6df274 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -4,7 +4,7 @@ import androidx.annotation.IdRes import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.store import com.tangem.wallet.R @@ -102,8 +102,8 @@ class BalanceWidget( groupBaseCurrency.show() tvCurrency.text = tokenWalletData?.currency?.currencySymbol tvBaseCurrency.text = data.currency.currencyName - tvAmount.text = if (showAmount) tokenWalletData?.getFormattedAmount() else "" - tvBaseAmount.text = if (showAmount) data.getFormattedAmount() else "" + tvAmount.text = if (showAmount) tokenWalletData?.getFormattedCryptoAmount() else "" + tvBaseAmount.text = if (showAmount) data.getFormattedCryptoAmount() else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = tokenWalletData?.getFormattedFiatAmount(store.state.globalState.appCurrency) @@ -113,7 +113,7 @@ class BalanceWidget( private fun showBalanceWithoutToken(data: WalletDataModel, showAmount: Boolean) = with(binding.lBalance) { groupBaseCurrency.hide() tvCurrency.text = data.currency.currencyName - tvAmount.text = if (showAmount) data.getFormattedAmount() else "" + tvAmount.text = if (showAmount) data.getFormattedCryptoAmount() else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = data.getFormattedFiatAmount(store.state.globalState.appCurrency) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index c6cad8e1bc..1c0c5027f2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -59,7 +59,7 @@ import com.tangem.tap.features.wallet.ui.images.load import com.tangem.tap.features.wallet.ui.test.TestWallet import com.tangem.tap.features.wallet.ui.utils.assembleWarnings import com.tangem.tap.features.wallet.ui.utils.getAvailableActions -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell @@ -425,7 +425,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = walletData.getFormattedAmount() + lBalance.tvAmount.text = walletData.getFormattedCryptoAmount() lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency) lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading) } @@ -437,7 +437,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = walletData.getFormattedAmount() + lBalance.tvAmount.text = walletData.getFormattedCryptoAmount() lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency) when (status) { is WalletDataModel.VerifiedOnline, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 4aec238877..e0a15c7e4b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -15,7 +15,7 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.ui.images.load -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatRate import com.tangem.tap.store @@ -85,7 +85,7 @@ class WalletAdapter : ListAdapter Date: Wed, 12 Jul 2023 09:21:30 +0300 Subject: [PATCH 043/100] Updated on 2026-08-14 --- .../wallet2/viewmodel/SeedPhraseViewModel.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index 2e6e7587eb..8dcb8ff0d6 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -169,13 +169,17 @@ class SeedPhraseViewModel @Inject constructor( if (fieldState.isError != hasError) { updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) } } - - val allFieldsWithoutError = SeedPhraseField.values() + val isCreateWalletButtonEnabled = SeedPhraseField.values() .map { field -> field.getState(uiState) } - .all { fieldState -> !fieldState.isError } + .all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError } - if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) { - updateUi { uiBuilder.checkSeedPhrase.updateCreateWalletButton(uiState, allFieldsWithoutError) } + if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) { + updateUi { + uiBuilder.checkSeedPhrase.updateCreateWalletButton( + uiState = uiState, + enabled = isCreateWalletButtonEnabled, + ) + } } } } From 929d547a70f7c0b0e74acd8b663ed2d481e26fb5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jul 2023 08:15:28 +0300 Subject: [PATCH 044/100] Updated on 2026-08-14 --- .../tangem/core/ui/components/buttons/common/TangemButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index b0a5b59116..c995a22c0e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -30,7 +30,7 @@ internal fun TangemButton( elevation = elevation, shape = size.toShape(), colors = colors, - contentPadding = size.toContentPadding(icon = icon), + contentPadding = if (showProgress) ButtonDefaults.ContentPadding else size.toContentPadding(icon = icon), ) { ButtonContent( text = text, From 982d3731e0b2d15edacbf5830c03842f6f975b46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jul 2023 14:56:53 +0500 Subject: [PATCH 045/100] Updated on 2026-08-14 --- .../buttons/HorizontalActionChips.kt | 88 +++++++++++++++++++ .../presentation/common/WalletPreviewData.kt | 4 +- .../wallet/state/WalletStateHolder.kt | 3 +- .../presentation/wallet/ui/WalletScreen.kt | 8 +- .../singlecurrency/WalletManageButtons.kt | 67 -------------- 5 files changed, 97 insertions(+), 73 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt new file mode 100644 index 0000000000..1646791082 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -0,0 +1,88 @@ +package com.tangem.core.ui.components.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.actions.ActionButton +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun HorizontalActionChips( + buttons: ImmutableList, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), +) { + LazyRow( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + contentPadding = contentPadding, + ) { + items( + items = buttons, + key = { it.text }, + itemContent = { ActionButton(config = it) }, + ) + } +} + +@Preview +@Composable +private fun Preview_HorizontalActionChips_Light( + @PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList, +) { + TangemTheme(isDark = false) { + HorizontalActionChips(buttons = buttons) + } +} + +@Preview +@Composable +private fun Preview_HorizontalActionChips_Dark( + @PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList, +) { + TangemTheme(isDark = true) { + HorizontalActionChips(buttons = buttons) + } +} + +private class ActionButtonConfigProvider : CollectionPreviewParameterProvider( + collection = persistentListOf( + ActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Receive", + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Exchange", + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = {}, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index dab4813f30..f535f5d114 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -200,7 +200,7 @@ internal object WalletPreviewData { ), ) - val manageButtons = persistentListOf( + private val manageButtons = persistentListOf( WalletManageButton.Buy(onClick = {}), WalletManageButton.Send(onClick = {}), WalletManageButton.Receive(onClick = {}), @@ -294,7 +294,7 @@ internal object WalletPreviewData { ), ), notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), - buttons = manageButtons, + buttons = manageButtons.map(WalletManageButton::config).toPersistentList(), marketPriceBlockState = MarketPriceBlockState.Content( currencyName = "BTC", price = "98900.12$", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index b9b216a198..12fb96d14e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import kotlinx.collections.immutable.ImmutableList @@ -58,7 +59,7 @@ internal sealed class WalletStateHolder( override val walletsListConfig: WalletsListConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, - val buttons: ImmutableList, + val buttons: ImmutableList, val marketPriceBlockState: MarketPriceBlockState, ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index b89486d90e..e281fb492e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.Transaction @@ -33,7 +34,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletManageButtons import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -73,9 +73,11 @@ internal fun WalletScreen(state: WalletStateHolder) { if (state is WalletStateHolder.SingleCurrencyContent) { item { - WalletManageButtons( + HorizontalActionChips( buttons = state.buttons, - modifier = changeableItemModifier.padding(top = TangemTheme.dimens.spacing14), + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt deleted file mode 100644 index 19b1a57045..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.buttons.actions.ActionButton -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import kotlinx.collections.immutable.ImmutableList - -/** - * Wallet manage buttons - * - * @param buttons manage buttons - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletManageButtons(buttons: ImmutableList, modifier: Modifier = Modifier) { - LazyRow( - modifier = modifier - .background(color = TangemTheme.colors.background.secondary) - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - items( - items = buttons, - key = { it.config.text }, - itemContent = { ActionButton(config = it.config) }, - ) - } -} - -@Preview -@Composable -private fun Preview_WalletManageButtons_Light( - @PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList, -) { - TangemTheme(isDark = false) { - WalletManageButtons(buttons = buttons) - } -} - -@Preview -@Composable -private fun Preview_WalletManageButtons_Dark( - @PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList, -) { - TangemTheme(isDark = true) { - WalletManageButtons(buttons = buttons) - } -} - -private class WalletManageButtonProvider : CollectionPreviewParameterProvider>( - collection = listOf(WalletPreviewData.manageButtons), -) \ No newline at end of file From 4a0978c18637192c976bc4c9e2f7ebe3d078ea8b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jul 2023 16:51:53 +0300 Subject: [PATCH 046/100] Updated on 2026-08-14 --- .../features/tokens/impl/presentation/TokensListFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index 8af509f84f..417a308de0 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.transition.TransitionInflater +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel @@ -40,6 +41,10 @@ internal class TokensListFragment : Fragment() { } TangemTheme { + val statusBarColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(color = statusBarColor) + } TokensListScreen( modifier = Modifier.systemBarsPadding(), stateHolder = viewModel.uiState, From c82c2b3cfba4809daaa5f31a82f303c11a140596 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jul 2023 21:11:12 +0800 Subject: [PATCH 047/100] Updated on 2026-08-14 --- .../common/analytics/topup/TopUpController.kt | 2 +- .../di/domain/wallets/WalletsDomainModule.kt | 7 ++++ .../common/feedback/AdditionalFeedbackInfo.kt | 9 ++--- .../tangem/tap/di/domain/CardDomainModule.kt | 28 +++++++++++++++ .../com/tangem/tap/domain/extensions/Card.kt | 23 ++---------- .../tap/domain/tokens/UserTokensRepository.kt | 2 +- .../details/redux/DetailsMiddleware.kt | 4 +-- .../tap/features/home/redux/HomeMiddleware.kt | 2 +- .../features/onboarding/OnboardingHelper.kt | 2 +- .../twins/redux/TwinCardsMiddleware.kt | 2 +- .../products/twins/ui/TwinsCardsFragment.kt | 10 ++---- .../redux/OnboardingWalletMiddleware.kt | 2 +- .../saveWallet/redux/SaveWalletMiddleware.kt | 2 +- .../tap/features/wallet/redux/WalletState.kt | 17 +-------- .../redux/middlewares/WalletMiddleware.kt | 2 +- .../wallet/redux/reducers/WalletReducer.kt | 8 +++-- .../redux/WalletSelectorMiddleware.kt | 4 +-- .../welcome/redux/WelcomeMiddleware.kt | 2 +- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 2 +- data/card/build.gradle.kts | 1 + .../card/DefaultCardSdkConfigRepository.kt | 9 +++-- .../com/tangem/data/card/di/CardDataModule.kt | 11 ++++-- .../card/GetAccessCodeSavingStatusUseCase.kt | 15 ++++++++ .../domain/card/GetBiometricsStatusUseCase.kt | 15 ++++++++ .../card/SetAccessCodeRequestPolicyUseCase.kt | 17 +++++++++ .../repository/CardSdkConfigRepository.kt | 3 ++ domain/legacy/build.gradle.kts | 1 + .../com/tangem/domain/userwallets/Artwork.kt | 14 ++++++++ .../userwallets}/GetCardImageUseCase.kt | 3 +- .../domain/userwallets}/UserWalletBuilder.kt | 6 ++-- .../userwallets}/UserWalletIdBuilder.kt | 2 +- domain/wallets/build.gradle.kts | 1 + .../domain/wallets/models/SaveWalletError.kt | 10 ++++++ .../wallets/usecase/SaveWalletUseCase.kt | 28 +++++++++++++++ features/wallet/impl/build.gradle.kts | 1 + .../wallet/viewmodels/WalletViewModel.kt | 36 +++++++++++++++++-- 36 files changed, 223 insertions(+), 80 deletions(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt rename {app/src/main/java/com/tangem/tap/domain/userWalletList => domain/legacy/src/main/java/com/tangem/domain/userwallets}/GetCardImageUseCase.kt (95%) rename {app/src/main/java/com/tangem/tap/domain/model/builders => domain/legacy/src/main/java/com/tangem/domain/userwallets}/UserWalletBuilder.kt (88%) rename {app/src/main/java/com/tangem/tap/domain/model/builders => domain/legacy/src/main/java/com/tangem/domain/userwallets}/UserWalletIdBuilder.kt (98%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt index bdd315bc3d..ba23fa52dd 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt @@ -9,6 +9,7 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.converters.TopUpEventConverter @@ -17,7 +18,6 @@ import com.tangem.tap.common.extensions.copy import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt index f945462b83..bcf08adf87 100644 --- a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.di.domain.wallets import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +18,10 @@ object WalletsDomainModule { fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + + @Provides + @ViewModelScoped + fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { + return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 7d138f1610..947691b26f 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -1,17 +1,12 @@ package com.tangem.tap.common.feedback import android.os.Build -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder class AdditionalFeedbackInfo { class EmailWalletInfo( diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 27d0e3e8df..4af9f75d50 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -1,6 +1,10 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase +import com.tangem.domain.card.GetBiometricsStatusUseCase import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import dagger.Module import dagger.Provides @@ -15,4 +19,28 @@ internal object CardDomainModule { @Provides @Singleton fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() + + @Provides + @Singleton + fun provideGetBiometricsStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetBiometricsStatusUseCase { + return GetBiometricsStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @Singleton + fun provideSetAccessCodeRequestPolicyUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): SetAccessCodeRequestPolicyUseCase { + return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @Singleton + fun provideGetAccessCodeSavingStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetAccessCodeSavingStatusUseCase { + return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index f963c6f982..363565bf3e 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -1,24 +1,17 @@ package com.tangem.tap.domain.extensions -import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.userwallets.Artwork import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.features.wallet.redux.Artwork val CardDTO.remainingSignatures: Int? get() = this.wallets.firstOrNull()?.remainingSignatures -val CardDTO.isWalletDataSupported: Boolean - get() = this.firmwareVersion.major >= 4 - -val CardDTO.isFirmwareMultiwalletAllowed: Boolean - get() = firmwareVersion >= FirmwareVersion.MultiWalletAvailable && settings.maxWalletsCount > 1 - val CardDTO.isHdWalletAllowedByApp: Boolean get() = settings.isHDWalletAllowed @@ -58,16 +51,4 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result ifAnyError() } -} - -fun CardDTO.getArtworkUrl(artworkId: String?): String? { - return when { - artworkId != null -> { - OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) - } - - cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL - cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL - else -> null - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index c4c01684bf..6842f8a94c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -10,7 +10,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.files.AndroidFileReader import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.models.scan.CardDTO -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.domain.tokens.converters.CurrencyConverter import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index e665b7cee3..478d0cb1bd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -12,6 +12,8 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -23,8 +25,6 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.userWalletList.isLockedSync diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 547440e96f..15a3f5c172 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -19,7 +20,6 @@ import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 477ab679c6..baf5ed2fcf 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -10,12 +10,12 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.setContext -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index af0338bb49..b768c53091 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -19,7 +20,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index 77bc7f7310..2fa0a10909 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -17,15 +17,12 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.Artwork import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.beginDelayedTransition -import com.tangem.tap.common.extensions.getDrawableCompat -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget @@ -35,7 +32,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletM import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index fe5800266a..2057707f3d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -13,6 +13,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.Artwork import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.operations.backup.BackupService @@ -29,7 +30,6 @@ import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.wallet.models.toCurrencies -import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 370e759dff..8261f5bef3 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -18,7 +19,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.isLockable import com.tangem.tap.features.wallet.redux.WalletAction diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 2135a4ab7d..0048c80636 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.wallet.redux -import android.graphics.Bitmap import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.common.entities.Button import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState @@ -118,19 +118,4 @@ enum class ErrorType { sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { class SendButton(enabled: Boolean) : WalletMainButton(enabled) -} - -data class Artwork( - val artworkId: String, - val artwork: Bitmap? = null, -) { - companion object { - const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" - const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" - const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" - const val SERGIO_CARD_ID = "BC01" - const val MARTA_CARD_ID = "BC02" - const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" - const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 2bdd72972f..a96e7e6197 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -12,6 +12,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.userwallets.GetCardImageUseCase import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -23,7 +24,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.userWalletList.GetCardImageUseCase import com.tangem.tap.domain.userWalletList.lockIfLockable import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index ac44f59413..e91e0a1194 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -4,12 +4,16 @@ import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.ErrorType +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.userWalletsListManager import org.rekotlin.Action @@ -115,7 +119,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS if (selectedUserWallet == action.userWalletId) { newState = newState.copy( - cardImage = Artwork(artworkId = action.url, artwork = newState.cardImage?.artwork), + cardImage = Artwork(artworkId = action.url), ) } } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index a331a52719..acac8cd6a8 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -6,6 +6,8 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.* @@ -18,8 +20,6 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index c10e1bcad2..f18499bf50 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -8,6 +8,7 @@ import com.tangem.common.flatMap import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -15,7 +16,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.signin.redux.SignInAction diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 1938886ede..432dc559a1 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -10,6 +10,7 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NativeToken @@ -17,7 +18,6 @@ import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 69bbb44e78..26b3010f2e 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(deps.tangem.card.android) implementation(deps.tangem.card.core) + implementation(projects.data.source.preferences) implementation(projects.domain.card) implementation(projects.domain.models) } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index e263616c2f..321a48b0bf 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -5,18 +5,21 @@ import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.ProductType /** - * Implementation of repository for managing with CardSDK config + * Implementation of repository for managing of CardSDK config * - * @property cardSdkProvider CardSDK instance provider + * @property cardSdkProvider CardSDK instance provider + * @property preferencesDataSource application shared preferences * [REDACTED_AUTHOR] */ internal class DefaultCardSdkConfigRepository( private val cardSdkProvider: CardSdkProvider, + private val preferencesDataSource: PreferencesDataSource, ) : CardSdkConfigRepository { @Deprecated("Use CardSdkConfigRepository's methods instead of this property") @@ -50,4 +53,6 @@ internal class DefaultCardSdkConfigRepository( -> CardIdDisplayFormat.Full } } + + override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt index 4ed7adf268..11f63f4a31 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.card.di import com.tangem.data.card.DefaultCardSdkConfigRepository import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.domain.card.repository.CardSdkConfigRepository import dagger.Module import dagger.Provides @@ -15,7 +16,13 @@ internal object CardDataModule { @Provides @Singleton - fun provideCardSdkConfigRepository(cardSdkProvider: CardSdkProvider): CardSdkConfigRepository { - return DefaultCardSdkConfigRepository(cardSdkProvider = cardSdkProvider) + fun provideCardSdkConfigRepository( + cardSdkProvider: CardSdkProvider, + preferencesDataSource: PreferencesDataSource, + ): CardSdkConfigRepository { + return DefaultCardSdkConfigRepository( + cardSdkProvider = cardSdkProvider, + preferencesDataSource = preferencesDataSource, + ) } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt new file mode 100644 index 0000000000..538ac0f240 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Use case for getting access code saving status + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class GetAccessCodeSavingStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(): Boolean = cardSdkConfigRepository.isAccessCodeSavingEnabled() +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt new file mode 100644 index 0000000000..a917d6f691 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Check if config has biometrics request policy + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class GetBiometricsStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(): Boolean = cardSdkConfigRepository.isBiometricsRequestPolicy() +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt new file mode 100644 index 0000000000..e10f624726 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Set access code request policy by 'isBiometricsRequestPolicy' + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class SetAccessCodeRequestPolicyUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(isBiometricsRequestPolicy: Boolean) { + cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index 11fa7e26e1..1190a2952d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -25,4 +25,7 @@ interface CardSdkConfigRepository { /** Update the card ID display format according to the [productType] of the scanned card */ fun updateCardIdDisplayFormat(productType: ProductType) + + /** Check if access code saving is enabled */ + fun isAccessCodeSavingEnabled(): Boolean } \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index e9449ad7de..525d2c8504 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":common")) implementation(project(":libs:auth")) implementation(project(":domain:models")) + implementation(projects.domain.wallets.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt new file mode 100644 index 0000000000..d788aed532 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.userwallets + +data class Artwork(val artworkId: String) { + + companion object { + const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" + const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" + const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" + const val SERGIO_CARD_ID = "BC01" + const val MARTA_CARD_ID = "BC02" + const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" + const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt index 9d218c840f..afa5eaaa37 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList +package com.tangem.domain.userwallets import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result @@ -6,7 +6,6 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.TangemApi -import com.tangem.tap.features.wallet.redux.Artwork /** * Use case for getting card image url diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt similarity index 88% rename from app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 7fa72a04fa..565930ae9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -1,12 +1,10 @@ -package com.tangem.tap.domain.model.builders +package com.tangem.domain.userwallets -import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.domain.userWalletList.GetCardImageUseCase class UserWalletBuilder( private val scanResponse: ScanResponse, @@ -24,7 +22,7 @@ class UserWalletBuilder( ProductType.Start2Coin -> "Start2Coin" ProductType.Wallet -> when { card.isBackupNotAllowed -> "Tangem card" - card.isStart2Coin -> "Start2Coin" + cardTypesResolver.isStart2Coin() -> "Start2Coin" else -> "Wallet" } } diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt index 30b4ef4407..cad0957152 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.model.builders +package com.tangem.domain.userwallets import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index a9a1449956..31807a9a48 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { // endregion // region Other libraries + implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) // endregion } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt new file mode 100644 index 0000000000..97ff31ef11 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.wallets.models + +/** +[REDACTED_AUTHOR] + */ +sealed interface SaveWalletError { + + // TODO: Finalize in next PRs + object CommonError : SaveWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt new file mode 100644 index 0000000000..3afdb23663 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.wallets.models.UserWallet + +/** + * Use case for saving user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { + requireNotNull(walletsStateHolder.userWalletsListManager).save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { return SaveWalletError.CommonError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index c216409615..55e9edc0c3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(deps.compose.reorderable) /** Other libraries */ + implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 9ee5a8a430..d47726ddeb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -9,8 +9,13 @@ import androidx.lifecycle.viewModelScope import arrow.core.Either import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase +import com.tangem.domain.card.GetBiometricsStatusUseCase import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWalletId @@ -35,8 +40,13 @@ import kotlin.properties.Delegates * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel internal class WalletViewModel @Inject constructor( + private val saveWalletUseCase: SaveWalletUseCase, + private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, + private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, + private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, private val scanCardProcessor: ScanCardProcessor, private val dispatchers: CoroutineDispatcherProvider, @@ -85,10 +95,32 @@ internal class WalletViewModel @Inject constructor( } private fun onScanCardClick() { + val prevRequestPolicyStatus = getBiometricsStatusUseCase() + + // Update access the code policy according access code saving status + setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) + viewModelScope.launch(dispatchers.io) { scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) - .doOnSuccess { /* [REDACTED_TODO_COMMENT] */ } - .doOnFailure { /* [REDACTED_TODO_COMMENT] */ } + .doOnSuccess { + // If card's public key is null then user wallet will be null + val userWallet = UserWalletBuilder(scanResponse = it).build() + + if (userWallet != null) { + saveWalletUseCase(userWallet) + .onLeft { + // Rollback policy if card saving was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } + } else { + // Rollback policy if card saving was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } + } + .doOnFailure { + // Rollback policy if card scanning was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } } } From 33f237c1a5037f4c8aabe3fb6a94a8f59d12e937 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jul 2023 10:52:46 +0300 Subject: [PATCH 048/100] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 8 + .../wallet/state/WalletPullToRefreshConfig.kt | 9 + .../wallet/state/WalletStateHolder.kt | 60 ++++--- .../presentation/wallet/ui/WalletScreen.kt | 159 ++++++++++-------- 4 files changed, 146 insertions(+), 90 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index f535f5d114..bc2443bc0e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -261,6 +261,10 @@ internal object WalletPreviewData { ), ), ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), notifications = persistentListOf( WalletNotification.UnreachableNetworks, WalletNotification.LikeTangemApp(onClick = {}), @@ -293,6 +297,10 @@ internal object WalletPreviewData { ), ), ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), buttons = manageButtons.map(WalletManageButton::config).toPersistentList(), marketPriceBlockState = MarketPriceBlockState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt new file mode 100644 index 0000000000..8aafa384f0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +/** + * Wallet screen top bar config + * + * @property isRefreshing state is indicator visible + * @property onRefresh lambda be invoked when pulled to refresh + */ +data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index 12fb96d14e..c4b9c37ee8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -7,11 +7,12 @@ import kotlinx.collections.immutable.ImmutableList /** * Wallet screen state holder * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications * [REDACTED_AUTHOR] */ @@ -19,6 +20,7 @@ internal sealed class WalletStateHolder( open val onBackClick: () -> Unit, open val topBarConfig: WalletTopBarConfig, open val walletsListConfig: WalletsListConfig, + open val pullToRefreshConfig: WalletPullToRefreshConfig, open val contentItems: ImmutableList, open val notifications: ImmutableList, ) { @@ -26,40 +28,58 @@ internal sealed class WalletStateHolder( /** * Multi currency wallet content state * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked */ data class MultiCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, val onOrganizeTokensClick: () -> Unit, - ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems, + notifications = notifications, + ) /** * Single currency wallet content state * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications - * @property buttons manage buttons - * @property marketPriceBlockState market price block state + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications + * @property buttons manage buttons + * @property marketPriceBlockState market price block state */ data class SingleCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, val buttons: ImmutableList, val marketPriceBlockState: MarketPriceBlockState, - ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems, + notifications = notifications, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index e281fb492e..42f5485b26 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -8,6 +8,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -44,6 +48,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat * [REDACTED_AUTHOR] */ +@OptIn(ExperimentalMaterialApi::class) @Suppress("LongMethod") @Composable internal fun WalletScreen(state: WalletStateHolder) { @@ -56,88 +61,102 @@ internal fun WalletScreen(state: WalletStateHolder) { val walletsListState = rememberLazyListState() val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState) + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = state.pullToRefreshConfig.onRefresh, + ) - LazyColumn( + Box( modifier = Modifier .padding(paddingValues = scaffoldPaddings) - .fillMaxSize(), - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.CenterHorizontally, + .pullRefresh(pullRefreshState), ) { - item { - WalletsList( - config = state.walletsListConfig, - lazyListState = walletsListState, - ) - } - - if (state is WalletStateHolder.SingleCurrencyContent) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.CenterHorizontally, + ) { item { - HorizontalActionChips( - buttons = state.buttons, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14), - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + WalletsList( + config = state.walletsListConfig, + lazyListState = walletsListState, ) } - } - items( - items = state.notifications, - itemContent = { item -> - Notification( - state = item.state, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - }, - ) - - if (state is WalletStateHolder.SingleCurrencyContent) { - item { - MarketPriceBlock( - state = state.marketPriceBlockState, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - } - } - - itemsIndexed( - items = state.contentItems, - key = { index, item -> - when (item) { - is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName - is WalletContentItemState.MultiCurrencyItem.Token -> index - is WalletContentItemState.SingleCurrencyItem.Title -> index - is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title - is WalletContentItemState.SingleCurrencyItem.Transaction -> index - is WalletContentItemState.Loading -> index + if (state is WalletStateHolder.SingleCurrencyContent) { + item { + HorizontalActionChips( + buttons = state.buttons, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) } - }, - itemContent = { index, item -> - ContentItem( - item = item, - modifier = changeableItemModifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = state.contentItems.lastIndex, - ), - ) - }, - ) + } - if (state is WalletStateHolder.MultiCurrencyContent) { - item { - OrganizeTokensButton( - onClick = state.onOrganizeTokensClick, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) + items( + items = state.notifications, + itemContent = { item -> + Notification( + state = item.state, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + }, + ) + + if (state is WalletStateHolder.SingleCurrencyContent) { + item { + MarketPriceBlock( + state = state.marketPriceBlockState, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + } + + itemsIndexed( + items = state.contentItems, + key = { index, item -> + when (item) { + is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName + is WalletContentItemState.MultiCurrencyItem.Token -> index + is WalletContentItemState.SingleCurrencyItem.Title -> index + is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title + is WalletContentItemState.SingleCurrencyItem.Transaction -> index + is WalletContentItemState.Loading -> index + } + }, + itemContent = { index, item -> + ContentItem( + item = item, + modifier = changeableItemModifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = state.contentItems.lastIndex, + ), + ) + }, + ) + + if (state is WalletStateHolder.MultiCurrencyContent) { + item { + OrganizeTokensButton( + onClick = state.onOrganizeTokensClick, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } } } + + PullRefreshIndicator( + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) } } } From 50c3b57a9113b21dc4bf5a65385e5a948fbe807f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jul 2023 18:02:01 +0300 Subject: [PATCH 049/100] Updated on 2026-08-14 --- .../com/tangem/domain/common/extensions/Blockchain.kt | 8 +++++++- gradle/dependencies.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index f4084f38df..15fb408bde 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -29,6 +29,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet "cardano" -> Blockchain.CardanoShelley "dogecoin" -> Blockchain.Dogecoin + "ducatus" -> Blockchain.Ducatus "litecoin" -> Blockchain.Litecoin "rootstock" -> Blockchain.RSK "stellar" -> Blockchain.Stellar @@ -86,6 +87,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.Cardano -> "cardano" Blockchain.CardanoShelley -> "cardano" Blockchain.Dogecoin -> "dogecoin" + Blockchain.Ducatus -> "ducatus" Blockchain.Ethereum -> "ethereum" Blockchain.EthereumTestnet -> "ethereum/test" Blockchain.EthereumClassic -> "ethereum-classic" @@ -150,6 +152,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom" Blockchain.Tron, Blockchain.TronTestnet -> "tron" Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot" + Blockchain.Ducatus -> "ducatus" Blockchain.Litecoin -> "litecoin" Blockchain.RSK -> "rootstock" Blockchain.Tezos -> "tezos" @@ -179,4 +182,7 @@ fun Blockchain.isSupportedInApp(): Boolean { return !excludedBlockchains.contains(this) } -private val excludedBlockchains = listOf(Blockchain.Unknown) \ No newline at end of file +private val excludedBlockchains = listOf( + Blockchain.Unknown, + Blockchain.Ducatus, +) \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d2f864a9ab..f5f9705ca1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-281" +tangemBlockchainSdk = "develop-283" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-273" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 3d2ef29a24e639717f335d40eab3ed4e842f0c1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jul 2023 21:55:50 +0500 Subject: [PATCH 050/100] Updated on 2026-08-14 --- features/tokendetails/impl/build.gradle.kts | 2 + .../di/TokenDetailsRouterModule.kt | 5 +- .../presentation/TokenDetailsFragment.kt | 46 ++++++ .../router/DefaultTokenDetailsRouter.kt | 15 +- .../router/InnerTokenDetailsRouter.kt | 8 + .../tokendetails/TokenDetailsPreviewData.kt | 41 +++++ .../tokendetails/state/TokenDetailsState.kt | 6 + .../state/TokenDetailsTopAppBarConfig.kt | 6 + .../tokendetails/state/TokenInfoBlockState.kt | 23 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 47 ++++++ .../ui/components/TokenDetailsTopAppBar.kt | 58 +++++++ .../ui/components/TokenInfoBlock.kt | 141 ++++++++++++++++++ .../viewmodels/TokenDetailsViewModel.kt | 30 ++++ 13 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index aa624c46ac..604cb6ce5e 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) /** DI */ implementation(deps.hilt.android) @@ -32,6 +33,7 @@ dependencies { /** Core modules */ implementation(projects.core.featuretoggles) implementation(projects.core.ui) + implementation(projects.core.navigation) /** Feature Apis */ implementation(projects.features.tokendetails.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt index 47a8b90f2e..3a0dc79713 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.di +import com.tangem.core.navigation.NavigationStateHolder import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -14,5 +15,7 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(): TokenDetailsRouter = DefaultTokenDetailsRouter() + fun provideTokenDetailsRouter(navigationStateHolder: NavigationStateHolder): TokenDetailsRouter { + return DefaultTokenDetailsRouter(navigationStateHolder) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt new file mode 100644 index 0000000000..d9e0a1bcf7 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.tokendetails.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class TokenDetailsFragment : Fragment() { + + @Inject + lateinit var tokenDetailsRouter: TokenDetailsRouter + + private val internalTokenDetailsRouter: InnerTokenDetailsRouter + get() = requireNotNull(tokenDetailsRouter as? InnerTokenDetailsRouter) { + "internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter" + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(inflater.context).apply { + setContent { + TangemTheme { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + val viewModel = hiltViewModel() + viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter + TokenDetailsScreen(state = viewModel.uiState) + } + } + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 6bfaf4dcaa..73819028da 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -1,10 +1,17 @@ package com.tangem.feature.tokendetails.presentation.router import androidx.fragment.app.Fragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment -internal class DefaultTokenDetailsRouter : TokenDetailsRouter { +internal class DefaultTokenDetailsRouter( + private val navigationStateHolder: NavigationStateHolder, +) : InnerTokenDetailsRouter { - // TODO: Doston fix it in next PRs - override fun getEntryFragment(): Fragment = Fragment() + override fun getEntryFragment(): Fragment = TokenDetailsFragment() + + override fun popBackStack() { + navigationStateHolder.navigate(NavigationAction.PopBackTo()) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt new file mode 100644 index 0000000000..89448deb0d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.tokendetails.presentation.router + +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter + +internal interface InnerTokenDetailsRouter : TokenDetailsRouter { + + fun popBackStack() +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cb065cf32f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails + +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R + +internal object TokenDetailsPreviewData { + + val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {}) + + val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( + name = "Stellar (XLM) with long name test", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + currency = TokenInfoBlockState.Currency.Native, + ) + val tokenInfoBlockStateWithLongName = TokenInfoBlockState( + name = "Tether (USDT) with long name test", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + currency = TokenInfoBlockState.Currency.Token( + networkName = "ERC20", + networkIcon = R.drawable.img_eth_22, + blockchainName = "Ethereum", + ), + ) + + val tokenInfoBlockState = TokenInfoBlockState( + name = "Tether USDT", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + currency = TokenInfoBlockState.Currency.Token( + networkName = "ERC20", + networkIcon = R.drawable.img_eth_22, + blockchainName = "Ethereum", + ), + ) + + val tokenDetailsState = TokenDetailsState( + topAppBarConfig = tokenDetailsTopAppBarConfig, + tokenInfoBlockState = tokenInfoBlockState, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt new file mode 100644 index 0000000000..438b92545d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +data class TokenDetailsState( + val topAppBarConfig: TokenDetailsTopAppBarConfig, + val tokenInfoBlockState: TokenInfoBlockState, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt new file mode 100644 index 0000000000..8ed33f724b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +data class TokenDetailsTopAppBarConfig( + val onBackClick: () -> Unit, + val onMoreClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt new file mode 100644 index 0000000000..ac4c80561c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.annotation.DrawableRes + +data class TokenInfoBlockState( + val name: String, + val iconUrl: String, + val currency: Currency, +) { + sealed class Currency { + object Native : Currency() + + /** + * @param networkName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. + * @param blockchainName - token's blockchain name. Ethereum, Tron and etc. + */ + data class Token( + val networkName: String, + val blockchainName: String, + @DrawableRes val networkIcon: Int, + ) : Currency() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt new file mode 100644 index 0000000000..380d2ad51f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock + +@Composable +internal fun TokenDetailsScreen(state: TokenDetailsState) { + Scaffold( + topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + Column( + modifier = Modifier + .padding(paddingValues = scaffoldPaddings) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxSize(), + ) { + TokenInfoBlock(state = state.tokenInfoBlockState) + } + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsScreen_LightTheme() { + TangemTheme(isDark = false) { + TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsScreen_DarkTheme() { + TangemTheme(isDark = true) { + TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt new file mode 100644 index 0000000000..a7901787f0 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -0,0 +1,58 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.features.tokendetails.impl.R +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { + TopAppBar( + navigationIcon = { + IconButton(onClick = config.onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Back", + ) + } + }, + title = {}, + actions = { + IconButton(onClick = config.onMoreClick) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vertical_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "More", + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + titleContentColor = TangemTheme.colors.icon.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + ) +} + +@Preview +@Composable +private fun Preview_TokenDetailsTopAppBar_LightTheme() { + TangemTheme(isDark = false) { + TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsTopAppBar_DarkTheme() { + TangemTheme(isDark = true) { + TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt new file mode 100644 index 0000000000..96ac8f6bf3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -0,0 +1,141 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.rememberAsyncImagePainter +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) { + Row(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier.weight(1F), + ) { + Text( + text = state.name, + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + NetworkInfoText(state.currency) + } + + val tokenIconPainter = when (LocalInspectionMode.current) { + // show drawable res in preview + true -> painterResource(id = R.drawable.img_stellar_22) + false -> rememberAsyncImagePainter(model = state.iconUrl) + } + + Image( + modifier = Modifier.size(TangemTheme.dimens.size48), + painter = tokenIconPainter, + contentDescription = null, + ) + } +} + +@Composable +private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { + when (currency) { + TokenInfoBlockState.Currency.Native -> { + Text( + text = stringResource(id = R.string.common_main_network), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption, + ) + } + is TokenInfoBlockState.Currency.Token -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + val state = extractNetwork(tokenCurrency = currency) + Text( + text = state.normalText, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = currency.networkIcon), + tint = Color.Unspecified, + contentDescription = null, + ) + Text( + text = state.boldText, + style = TangemTheme.typography.caption.copy(fontWeight = FontWeight.Medium), + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +private const val SEPARATOR = " %image% " + +@Composable +private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText { + val splitString = stringResource( + id = R.string.token_details_token_type_subtitle, + formatArgs = arrayOf( + tokenCurrency.networkName, + tokenCurrency.blockchainName, + ), + ).split(SEPARATOR) + + return remember(splitString) { + ExtractedTokenNetworkText( + normalText = splitString.firstOrNull().orEmpty(), + boldText = splitString.getOrNull(1).orEmpty(), + ) + } +} + +private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String) + +@Preview +@Composable +private fun Preview_TokenInfoBlock_LightTheme( + @PreviewParameter(TokenInfoStateProvider::class) + state: TokenInfoBlockState, +) { + TangemTheme(isDark = false) { + TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) + } +} + +@Preview +@Composable +private fun Preview_TokenInfoBlock_DarkTheme( + @PreviewParameter(TokenInfoStateProvider::class) + state: TokenInfoBlockState, +) { + TangemTheme(isDark = true) { + TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) + } +} + +private class TokenInfoStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenDetailsPreviewData.tokenInfoBlockState, + TokenDetailsPreviewData.tokenInfoBlockStateWithLongName, + TokenDetailsPreviewData.tokenInfoBlockStateWithLongNameInMainCurrency, + ), +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt new file mode 100644 index 0000000000..50e489eedb --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlin.properties.Delegates + +@HiltViewModel +internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { + + var router: InnerTokenDetailsRouter by Delegates.notNull() + + var uiState by mutableStateOf(getInitialState()) + private set + + private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy( + topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy( + onBackClick = ::onBackClick, + ), + ) + + private fun onBackClick() { + router.popBackStack() + } +} \ No newline at end of file From 4430add6f05ee7ec7e2a40b23253b5f1fc583bde Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 14:15:27 +0500 Subject: [PATCH 051/100] Updated on 2026-08-14 --- .../buttons/HorizontalActionChips.kt | 8 +- .../core/ui/utils/BigDecimalFormatter.kt | 2 + features/tokendetails/impl/build.gradle.kts | 2 + .../tokendetails/TokenDetailsPreviewData.kt | 38 +++++ .../state/TokenDetailsBalanceBlockState.kt | 23 +++ .../tokendetails/state/TokenDetailsState.kt | 1 + .../tokendetails/ui/TokenDetailsScreen.kt | 4 + .../ui/components/TokenDetailsBalanceBlock.kt | 136 ++++++++++++++++++ .../viewmodels/TokenDetailsViewModel.kt | 13 ++ 9 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index 1646791082..de7706a43d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -1,6 +1,5 @@ package com.tangem.core.ui.components.buttons -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -12,7 +11,6 @@ import androidx.compose.ui.Modifier 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 com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig @@ -24,12 +22,10 @@ import kotlinx.collections.immutable.persistentListOf fun HorizontalActionChips( buttons: ImmutableList, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0), ) { LazyRow( - modifier = modifier - .background(color = TangemTheme.colors.background.secondary) - .fillMaxWidth(), + modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index d10357cd54..2a78274eb5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -8,6 +8,8 @@ import java.util.Locale object BigDecimalFormatter { + const val EMPTY_BALANCE_SIGN = "—" + private const val TEMP_CURRENCY_CODE = "USD" fun formatCryptoAmount( diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 604cb6ce5e..d3af011dcd 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -26,6 +26,8 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.coil) + implementation(deps.kotlin.immutable.collections) + /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) 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 cb065cf32f..c94cca8569 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 @@ -1,9 +1,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList internal object TokenDetailsPreviewData { @@ -34,8 +38,42 @@ internal object TokenDetailsPreviewData { ), ) + private val actionButtons = persistentListOf( + ActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Receive", + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Exchange", + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = {}, + ), + ) + + private val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList() + + val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons) + val balanceContent = TokenDetailsBalanceBlockState.Content( + actionButtons = actionButtons, + fiatBalance = "123,00$", + cryptoBalance = "866,96 USDT", + ) + val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons) + val tokenDetailsState = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, + tokenBalanceBlockState = balanceLoading, ) } \ No newline at end of file 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 new file mode 100644 index 0000000000..a67edd1a61 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import kotlinx.collections.immutable.ImmutableList + +sealed class TokenDetailsBalanceBlockState { + + abstract val actionButtons: ImmutableList + + data class Loading( + override val actionButtons: ImmutableList, + ) : TokenDetailsBalanceBlockState() + + data class Content( + override val actionButtons: ImmutableList, + val fiatBalance: String, + val cryptoBalance: String, + ) : TokenDetailsBalanceBlockState() + + data class Error( + override val actionButtons: ImmutableList, + ) : TokenDetailsBalanceBlockState() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 438b92545d..988dd22bfa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -3,4 +3,5 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, + val tokenBalanceBlockState: TokenDetailsBalanceBlockState, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 380d2ad51f..bd876c400d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -10,6 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock @@ -24,8 +26,10 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { .padding(paddingValues = scaffoldPaddings) .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { TokenInfoBlock(state = state.tokenInfoBlockState) + TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) } } } 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 new file mode 100644 index 0000000000..df54b41461 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -0,0 +1,136 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ) { + Column { + Text( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + text = stringResource(id = R.string.onboarding_balance_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + ) + FiatBalance( + state = state, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = TangemTheme.dimens.spacing12), + ) + CryptoBalance( + state = state, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = TangemTheme.dimens.spacing12), + ) + + HorizontalActionChips( + buttons = state.actionButtons, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + when (state) { + is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size102, + height = TangemTheme.dimens.size24, + ), + ) + is TokenDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.fiatBalance, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + is TokenDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + when (state) { + is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size70, + height = TangemTheme.dimens.size16, + ), + ) + is TokenDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.cryptoBalance, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.primary1, + ) + is TokenDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsBalanceBlock_LightTheme( + @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, +) { + TangemTheme(isDark = false) { + TokenDetailsBalanceBlock(state) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsBalanceBlock_DarkTheme( + @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, +) { + TangemTheme(isDark = true) { + TokenDetailsBalanceBlock(state) + } +} + +private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenDetailsPreviewData.balanceLoading, + TokenDetailsPreviewData.balanceContent, + TokenDetailsPreviewData.balanceError, + ), +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 50e489eedb..bcbf76c58a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -4,12 +4,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates +private const val LOADING_DELAY = 4_000L + @HiltViewModel internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { @@ -18,6 +23,14 @@ internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { var uiState by mutableStateOf(getInitialState()) private set + init { + // simulate loading state + viewModelScope.launch { + delay(LOADING_DELAY) + uiState = uiState.copy(tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent) + } + } + private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy( topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy( onBackClick = ::onBackClick, From 66aedc97cbb0066e0da3f848c6c73f6f3a33f8d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 13:01:32 +0300 Subject: [PATCH 052/100] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Buttons.kt | 14 ++- .../com/tangem/core/ui/components/Notifier.kt | 68 +++++++++++++ .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../com/tangem/core/ui/res/TangemShapes.kt | 2 + .../domain/DefaultSeedPhraseInteractor.kt | 15 ++- .../onboarding/domain/SeedPhraseInteractor.kt | 3 +- .../model/OnboardingSeedPhraseState.kt | 2 +- .../wallet2/ui/ImportSeedPhraseScreen.kt | 97 ++++++++++++++++--- .../ImportSeedPhraseStateBuilder.kt | 15 +-- .../wallet2/viewmodel/SeedPhraseViewModel.kt | 3 +- 10 files changed, 192 insertions(+), 28 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 11682ec1f1..385824799e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -230,7 +230,12 @@ private fun PrimaryButtonSample() { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { PrimaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { }) - PrimaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { }) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { }, + ) PrimaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), text = "Manage tokens", @@ -289,7 +294,12 @@ private fun SecondaryButtonSample() { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { SecondaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { }) - SecondaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { }) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { }, + ) SecondaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), text = "Manage tokens", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt new file mode 100644 index 0000000000..a646f37f33 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt @@ -0,0 +1,68 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=WSV3AxC6zV1y0CHF-1) + * */ +@Composable +fun Notifier( + text: String, + modifier: Modifier = Modifier, + textColor: Color = TangemTheme.colors.text.primary1, + backgroundColor: Color = TangemTheme.colors.icon.inactive, +) { + Box( + modifier = modifier + .heightIn(TangemTheme.dimens.size32) + .background( + color = backgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = TangemTheme.dimens.size10), + text = text, + color = textColor, + style = TangemTheme.typography.button, + ) + } +} + +@Preview +@Composable +private fun TangemNotifierPreview_Light(@PreviewParameter(NotifierProvider::class) text: String) { + TangemTheme(isDark = false) { + Notifier(text = text) + } +} + +@Preview +@Composable +private fun TangemNotifierPreview_Dark(@PreviewParameter(NotifierProvider::class) text: String) { + TangemTheme(isDark = true) { + Notifier(text = text) + } +} + +private class NotifierProvider : CollectionPreviewParameterProvider( + collection = listOf( + "a", + "Card 1 of 2", + "Card 1 of 2 or many other", + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index c3f075ffb0..a78bd58dbd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -43,6 +43,7 @@ data class TangemDimens internal constructor( val size5: Dp = 5.dp, val size7: Dp = 7.dp, val size8: Dp = 8.dp, + val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, val size16: Dp = 16.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt index b452e38004..a3802221fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Shape data class TangemShapes internal constructor( val roundedCornersSmall: Shape, val roundedCornersSmall2: Shape, + val roundedCorners8: Shape, val roundedCornersMedium: Shape, val roundedCornersXMedium: Shape, val roundedCornersLarge: Shape, @@ -16,6 +17,7 @@ data class TangemShapes internal constructor( constructor(dimens: TangemDimens) : this( roundedCornersSmall = RoundedCornerShape(size = dimens.radius2), roundedCornersSmall2 = RoundedCornerShape(size = dimens.radius4), + roundedCorners8 = RoundedCornerShape(size = dimens.radius8), roundedCornersMedium = RoundedCornerShape(size = dimens.radius12), roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16), roundedCornersLarge = RoundedCornerShape(size = dimens.radius28), diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt index f3152c931e..a63b4bccea 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt @@ -5,6 +5,9 @@ import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult import com.tangem.feature.onboarding.data.MnemonicRepository import com.tangem.utils.extensions.isNotWhitespace +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList /** [REDACTED_AUTHOR] @@ -58,14 +61,18 @@ internal class DefaultSeedPhraseInteractor constructor( } } - override suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List { - if (text.isEmpty() || cursorPosition == 0 || hasSelection) return emptyList() + override suspend fun getSuggestions( + text: String, + hasSelection: Boolean, + cursorPosition: Int, + ): ImmutableList { + if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf() val word = partWordFinder.getLeadPartOfWord(text, cursorPosition) - ?: return emptyList() + ?: return persistentListOf() val suggestions = repository.getWordsDictionary() .filter { it.startsWith(word, ignoreCase = false) && it != word } - .toList() + .toPersistentList() return suggestions } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt index 43ed7ac068..2c0f319c19 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.domain import com.tangem.crypto.bip39.Mnemonic +import kotlinx.collections.immutable.ImmutableList /** [REDACTED_AUTHOR] @@ -10,7 +11,7 @@ interface SeedPhraseInteractor { suspend fun getMnemonicComponents(): Result> suspend fun isWordMatch(word: String): Boolean suspend fun validateMnemonicString(text: String): Result> - suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List + suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult companion object { diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt index 74b732e86b..44bdac25c3 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt @@ -52,7 +52,7 @@ data class ImportSeedPhraseState( val onSuggestedPhraseClick: (Int) -> Unit, val buttonCreateWallet: ButtonState, val invalidWords: Set = emptySet(), - val suggestionsList: List = emptyList(), + val suggestionsList: ImmutableList = persistentListOf(), val error: SeedPhraseError? = null, ) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index d29fd51838..be8a31f5df 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui import androidx.compose.animation.* +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.OutlinedTextField @@ -11,10 +12,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle +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.compose.ui.unit.IntOffset -import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.components.TangemTextFieldsDefault +import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState @@ -23,6 +29,8 @@ import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Onboardi import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** [REDACTED_AUTHOR] @@ -48,7 +56,9 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo state = state, ) SuggestionsBlock( - state = state, + modifier = Modifier.padding(top = TangemTheme.dimens.size4), + suggestionsList = state.suggestionsList, + onClick = { index -> state.onSuggestedPhraseClick(index) }, ) } } @@ -114,25 +124,86 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif @Suppress("ReusedModifierInstance") @Composable -private fun SuggestionsBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { +private fun SuggestionsBlock( + suggestionsList: ImmutableList, + onClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { AnimatedVisibility( enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), - visible = state.suggestionsList.isNotEmpty(), + visible = suggestionsList.isNotEmpty(), ) { LazyRow( - modifier = modifier.fillMaxSize(), + modifier = modifier, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), ) { - items(state.suggestionsList.size) { index -> - PrimaryButton( - modifier = Modifier - .height(TangemTheme.dimens.size46) - .padding(all = TangemTheme.dimens.size4), - text = state.suggestionsList[index], - onClick = { state.onSuggestedPhraseClick(index) }, + items(suggestionsList.size) { index -> + SuggestionButton( + modifier = Modifier.rowPadding( + index = index, + rowSize = suggestionsList.size, + outSide = TangemTheme.dimens.size0, + inSide = TangemTheme.dimens.size4, + ), + text = suggestionsList[index], + onClick = { onClick(index) }, ) } } } -} \ No newline at end of file +} + +@Composable +private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box(modifier = modifier.clickable(onClick = onClick)) { + Notifier( + text = text, + backgroundColor = TangemTheme.colors.background.action, + textColor = TangemTheme.colors.text.primary2, + ) + } +} + +private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) { + 0 -> this.padding(start = outSide, end = inSide) + rowSize - 1 -> this.padding(start = inSide, end = outSide) + else -> this.padding(horizontal = inSide) +} + +@Preview +@Composable +private fun SuggestionsBlockPreview_Light( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = false) { + SuggestionsBlock( + suggestionsList = suggestions, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun SuggestionsBlockPreview_Dark( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = true) { + SuggestionsBlock( + suggestionsList = suggestions, + onClick = {}, + ) + } +} + +private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>( + collection = listOf( + persistentListOf( + "one", + "each", + "explorer", + "unknown", + ), + ), +) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt index 5ffb9b5c41..794ec1d599 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.feature.onboarding.domain.InsertSuggestionResult import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState +import kotlinx.collections.immutable.ImmutableList /** [REDACTED_AUTHOR] @@ -39,12 +40,14 @@ class ImportSeedPhraseStateBuilder { ), ) - fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List): OnboardingSeedPhraseState = - uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - suggestionsList = suggestions, - ), - ) + fun updateSuggestions( + uiState: OnboardingSeedPhraseState, + suggestions: ImmutableList, + ): OnboardingSeedPhraseState = uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + suggestionsList = suggestions, + ), + ) fun insertSuggestionWord( uiState: OnboardingSeedPhraseState, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index 8dcb8ff0d6..c7d9c234ba 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -22,6 +22,7 @@ import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.extensions.isEven import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -373,7 +374,7 @@ class SeedPhraseViewModel @Inject constructor( ) updateUi { val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult) - uiBuilder.importSeedPhrase.updateSuggestions(mediateState, emptyList()) + uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf()) } } } From 5f1a5494dc4f86cbe86bd19128d66bb14438d387 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 15 Jul 2023 01:47:52 +0300 Subject: [PATCH 053/100] Updated on 2026-08-14 --- data/tokens/build.gradle.kts | 1 + .../repository/MockNetworksRepository.kt | 11 +- .../tokens/repository/MockQuotesRepository.kt | 8 +- .../tokens/repository/MockTokensRepository.kt | 18 +- .../com/tangem/domain/core/error/DataError.kt | 9 + .../domain/core/raise/DelegatedRaise.kt | 13 ++ .../domain/tokens/GetTokenListUseCase.kt | 55 ++--- .../domain/tokens/error/TokenListError.kt | 31 +++ .../tangem/domain/tokens/error/TokensError.kt | 21 -- .../TokenListFiatBalanceOperations.kt | 7 +- .../tokens/operations/TokenListOperations.kt | 188 ++++++++++++++++++ .../operations/TokenListSortingOperations.kt | 115 +++++++++++ .../TokenStatusOperations.kt | 16 +- .../TokensStatusesOperations.kt | 74 +++++-- .../tokens/repository/NetworksRepository.kt | 6 +- .../tokens/repository/QuotesRepository.kt | 4 +- .../tokens/repository/TokensRepository.kt | 10 +- .../tokens/utils/TokenListOperations.kt | 163 --------------- .../domain/tokens/GetTokenListUseCaseTest.kt | 59 +++--- .../repository/MockNetworksRepository.kt | 16 +- .../tokens/repository/MockQuotesRepository.kt | 10 +- .../tokens/repository/MockTokensRepository.kt | 25 +-- ...> TokenListErrorToWalletStateConverter.kt} | 8 +- .../wallet/viewmodels/WalletViewModel.kt | 10 +- 24 files changed, 525 insertions(+), 353 deletions(-) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{utils => operations}/TokenListFiatBalanceOperations.kt (94%) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{utils => operations}/TokenStatusOperations.kt (84%) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{utils => operations}/TokensStatusesOperations.kt (60%) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/{TokenErrorToWalletStateConverter.kt => TokenListErrorToWalletStateConverter.kt} (56%) diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 95abf3ed40..2d8aaaf35d 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -5,6 +5,7 @@ plugins { } dependencies { + implementation(projects.domain.core) implementation(projects.domain.tokens) implementation(projects.domain.wallets.models) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt index a328f0a544..26bc58aa70 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt @@ -1,9 +1,6 @@ package com.tangem.data.tokens.repository -import arrow.core.Either -import arrow.core.right import com.tangem.data.tokens.mock.MockNetworks -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Token @@ -14,23 +11,21 @@ import kotlinx.coroutines.flow.flowOf internal class MockNetworksRepository : NetworksRepository { - override fun getNetworks(networksIds: Set): Either> { + override fun getNetworks(networksIds: Set): Set { return MockNetworks.networks .filter { it.id in networksIds } .toSet() - .right() } override fun getNetworkStatuses( userWalletId: UserWalletId, networks: Map>, refresh: Boolean, - ): Flow>> { + ): Flow> { return flowOf( MockNetworks.networksStatuses .filter { it.networkId in networks.keys } - .toSet() - .right(), + .toSet(), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt index 1e6cbaa1d8..2510893558 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt @@ -1,9 +1,6 @@ package com.tangem.data.tokens.repository -import arrow.core.Either -import arrow.core.right import com.tangem.data.tokens.mock.MockQuotes -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.QuotesRepository @@ -12,12 +9,11 @@ import kotlinx.coroutines.flow.flowOf internal class MockQuotesRepository : QuotesRepository { - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> { + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { return flowOf( MockQuotes.quotes .filter { it.tokenId in tokensIds } - .toSet() - .right(), + .toSet(), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt index 92092f1046..e6b7760a7c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt @@ -1,10 +1,6 @@ package com.tangem.data.tokens.repository -import arrow.core.Either -import arrow.core.left -import arrow.core.right import com.tangem.data.tokens.mock.MockTokens -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId @@ -18,17 +14,17 @@ internal class MockTokensRepository : TokensRepository { sortedTokensIds: Set, isGrouped: Boolean, isSortedByBalance: Boolean, - ): Either = Unit.right() + ) = Unit - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> { - return flowOf(value = MockTokens.tokens[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return flowOf(value = MockTokens.tokens[userWalletId]!!) } - override fun isTokensGrouped(userWalletId: UserWalletId): Flow> { - return flowOf(MockTokens.isGrouped[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return flowOf(value = MockTokens.isGrouped[userWalletId]!!) } - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> { - return flowOf(MockTokens.isSortedByBalance[userWalletId]?.right() ?: TokensError.EmptyTokens.left()) + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return flowOf(value = MockTokens.isSortedByBalance[userWalletId]!!) } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt new file mode 100644 index 0000000000..11567ca43b --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.core.error + +sealed class DataError : Exception() { + + sealed class NetworkError : DataError() { + + object NoInternetConnection : NetworkError() + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt new file mode 100644 index 0000000000..ffde660eb6 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.core.raise + +import arrow.core.raise.Raise + +abstract class DelegatedRaise( + private val otherRaise: Raise, + private val transformError: (Error) -> OtherError, +) : Raise { + + override fun raise(r: Error): Nothing { + otherRaise.raise(transformError(r)) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 024c43ef42..3f42be91fc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -5,15 +5,14 @@ import arrow.core.left import arrow.core.raise.Raise import arrow.core.raise.recover import arrow.core.right -import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.operations.TokensStatusesOperations import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository -import com.tangem.domain.tokens.utils.TokenListFiatBalanceOperations -import com.tangem.domain.tokens.utils.TokenListOperations -import com.tangem.domain.tokens.utils.TokensStatusesOperations import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -21,13 +20,13 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flatMapConcat class GetTokenListUseCase( - private val tokensRepository: TokensRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val dispatchers: CoroutineDispatcherProvider, + internal val tokensRepository: TokensRepository, + internal val quotesRepository: QuotesRepository, + internal val networksRepository: NetworksRepository, + internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { return channelFlow { recover( block = { @@ -41,53 +40,39 @@ class GetTokenListUseCase( ) } } - private fun Raise.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { + private fun Raise.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens -> createTokenList(userWalletId, tokens) } } - private fun Raise.getTokensStatuses( + private fun Raise.getTokensStatuses( userWalletId: UserWalletId, refresh: Boolean, ): Flow> { val operations = TokensStatusesOperations( - tokensRepository, - quotesRepository, - networksRepository, - userWalletId, - refresh, - dispatchers, + userWalletId = userWalletId, + refresh = refresh, + useCase = this@GetTokenListUseCase, raise = this, + transformError = TokenListError::fromTokenStatusesOperations, ) return operations.getTokensStatusesFlow() } - private suspend fun Raise.createTokenList( + private fun Raise.createTokenList( userWalletId: UserWalletId, tokens: Set, ): Flow { - val isAnyTokenLoading = tokens.any { it.value is TokenStatus.Loading } val operations = TokenListOperations( - tokensRepository, - networksRepository, - userWalletId, - calculateFiatBalance(tokens, isAnyTokenLoading), - isAnyTokenLoading, - dispatchers, + userWalletId = userWalletId, + tokens = tokens, + useCase = this@GetTokenListUseCase, raise = this, + transform = TokenListError::fromTokenListOperations, ) - return operations.getTokenListFlow(tokens) - } - - private suspend fun calculateFiatBalance( - tokens: Set, - isAnyTokenLoading: Boolean, - ): TokenList.FiatBalance { - val operations = TokenListFiatBalanceOperations(tokens, isAnyTokenLoading, dispatchers) - - return operations.calculateFiatBalance() + return operations.getTokenListFlow() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt new file mode 100644 index 0000000000..06897839f6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.tokens.error + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.operations.TokensStatusesOperations + +sealed class TokenListError { + + object EmptyTokens : TokenListError() + + data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : TokenListError() + + data class DataError(val cause: Throwable) : TokenListError() + + internal companion object { + + fun fromTokenStatusesOperations(error: TokensStatusesOperations.Error): TokenListError = when (error) { + is TokensStatusesOperations.Error.DataError -> DataError(error.cause) + is TokensStatusesOperations.Error.EmptyNetworksStatuses, + is TokensStatusesOperations.Error.EmptyQuotes, + is TokensStatusesOperations.Error.EmptyTokens, + -> EmptyTokens + } + + fun fromTokenListOperations(error: TokenListOperations.Error): TokenListError = when (error) { + is TokenListOperations.Error.DataError -> DataError(error.cause) + is TokenListOperations.Error.UnableToSortTokenList -> UnableToSortTokenList(error.unsortedTokenList) + is TokenListOperations.Error.UnableToGroupTokenList -> UnableToSortTokenList(error.ungroupedTokenList) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt deleted file mode 100644 index 6571bc1d78..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.domain.tokens.error - -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.Token - -sealed class TokensError { - - object EmptyTokens : TokensError() - - object EmptyQuotes : TokensError() - - object EmptyNetworks : TokensError() - - object EmptyNetworkStatues : TokensError() - - data class TokenAmountNotFound(val tokenId: Token.ID) : TokensError() - - data class TokenFiatAmountLessThenZero(val tokenId: Token.ID) : TokensError() - - data class NetworkNotFound(val networkId: Network.ID) : TokensError() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt similarity index 94% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 6d3360512e..69378510f3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -1,5 +1,6 @@ -package com.tangem.domain.tokens.utils +package com.tangem.domain.tokens.operations +import arrow.core.NonEmptySet import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -7,7 +8,7 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal internal class TokenListFiatBalanceOperations( - private val tokens: Set, + private val tokens: NonEmptySet, private val isAnyTokenLoading: Boolean, private val dispatcher: CoroutineDispatcherProvider, ) { @@ -15,7 +16,7 @@ internal class TokenListFiatBalanceOperations( suspend fun calculateFiatBalance(): TokenList.FiatBalance { return withContext(dispatcher.single) { var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading - if (tokens.isEmpty() || isAnyTokenLoading) return@withContext fiatBalance + if (isAnyTokenLoading) return@withContext fiatBalance for (token in tokens) { when (val status = token.value) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt new file mode 100644 index 0000000000..87253155db --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -0,0 +1,188 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokenListOperations( + private val tokensRepository: TokensRepository, + private val networksRepository: NetworksRepository, + private val userWalletId: UserWalletId, + private val tokens: Set, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transform: (Error) -> E, +) : DelegatedRaise(raise, transform) { + + constructor( + userWalletId: UserWalletId, + tokens: Set, + useCase: GetTokenListUseCase, + raise: Raise, + transform: (Error) -> E, + ) : this( + tokensRepository = useCase.tokensRepository, + networksRepository = useCase.networksRepository, + userWalletId = userWalletId, + tokens = tokens, + dispatchers = useCase.dispatchers, + raise = raise, + transform = transform, + ) + + fun getTokenListFlow(): Flow { + return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> + createTokenList(isGrouped, isSortedByBalance) + } + } + + private suspend fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { + return withContext(dispatchers.single) { + val tokensNes = tokens.toNonEmptySetOrNull() + ?: return@withContext TokenList.NotInitialized + + val isAnyTokenLoading = tokensNes.any { it.value is TokenStatus.Loading } + val fiatBalanceOperations = TokenListFiatBalanceOperations(tokensNes, isAnyTokenLoading, dispatchers) + + createTokenList( + tokens = tokensNes, + fiatBalance = fiatBalanceOperations.calculateFiatBalance(), + isAnyTokenLoading = isAnyTokenLoading, + isGrouped = isGrouped, + isSortedByBalance = isSortedByBalance, + ) + } + } + + private suspend fun createTokenList( + tokens: NonEmptySet, + fiatBalance: TokenList.FiatBalance, + isAnyTokenLoading: Boolean, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): TokenList { + val sortingOperations = TokenListSortingOperations( + tokens = tokens, + isAnyTokenLoading = isAnyTokenLoading, + isSortedByBalance = isSortedByBalance, + dispatchers = dispatchers, + raise = this, + transformError = { e -> + Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } + }, + ) + + return createTokenList(tokens, sortingOperations, fiatBalance, isGrouped) + } + + private suspend fun createTokenList( + tokens: NonEmptySet, + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + isGrouped: Boolean, + ): TokenList { + return if (isGrouped) { + val networks = ensureNotNull(getNetworks(tokens).toNonEmptySetOrNull()) { + Error.UnableToGroupTokenList( + ungroupedTokenList = createUngroupedTokenList(sortingOperations, fiatBalance), + ) + } + + createGroupedTokenList(sortingOperations, fiatBalance, networks) + } else { + createUngroupedTokenList(sortingOperations, fiatBalance) + } + } + + private suspend fun getNetworks(tokensNes: NonEmptySet): Set { + return withContext(dispatchers.io) { + val networksIds = tokensNes.map { it.networkId }.toNonEmptySet() + catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + + private suspend fun createUngroupedTokenList( + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + ): TokenList.Ungrouped = TokenList.Ungrouped( + sortedBy = sortingOperations.getSortType(), + totalFiatBalance = fiatBalance, + tokens = sortingOperations.getTokens(), + ) + + private suspend fun createGroupedTokenList( + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + networks: NonEmptySet, + ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( + sortedBy = sortingOperations.getSortType(), + totalFiatBalance = fiatBalance, + groups = sortingOperations.getGroupedTokens(networks), + ) + + private fun createUnsortedUngroupedTokenList( + tokens: NonEmptySet, + fiatBalance: TokenList.FiatBalance, + ): TokenList.Ungrouped { + return TokenList.Ungrouped( + sortedBy = TokenList.SortType.NONE, + totalFiatBalance = fiatBalance, + tokens = tokens, + ) + } + + private fun getIsGrouped(): Flow { + return tokensRepository.isTokensGrouped(userWalletId) + .catch { raise(Error.DataError(it)) } + .onEmpty { emit(value = false) } + .flowOn(dispatchers.io) + } + + private fun getIsSortedByBalance(): Flow { + return tokensRepository.isTokensSortedByBalance(userWalletId) + .catch { raise(Error.DataError(it)) } + .onEmpty { emit(value = false) } + .flowOn(dispatchers.io) + } + + sealed class Error { + + data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() + + data class UnableToGroupTokenList(val ungroupedTokenList: TokenList.Ungrouped) : Error() + + data class DataError(val cause: Throwable) : Error() + + internal companion object { + + fun fromTokenListOperations( + e: TokenListSortingOperations.Error, + createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped, + ): Error = when (e) { + is TokenListSortingOperations.Error.EmptyNetworks, + is TokenListSortingOperations.Error.EmptyTokens, + is TokenListSortingOperations.Error.NetworkNotFound, + -> UnableToSortTokenList( + unsortedTokenList = createUnsortedUngroupedTokenList(), + ) + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt new file mode 100644 index 0000000000..5d1b4516bb --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -0,0 +1,115 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.raise.ensure +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListSortingOperations( + private val tokens: Set, + private val isAnyTokenLoading: Boolean, + private val isSortedByBalance: Boolean, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> E, +) : DelegatedRaise(raise, transformError) { + + suspend fun getGroupedTokens(networks: Set): NonEmptySet { + return withContext(dispatchers.single) { + ensure(tokens.isNotEmpty()) { Error.EmptyTokens } + val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { + Error.EmptyNetworks + } + + if (isSortedByBalance) { + groupAndSortTokensByBalance(networksNes) + } else { + groupTokens(networksNes) + } + } + } + + suspend fun getTokens(): NonEmptySet { + return withContext(dispatchers.single) { + val tokensNes = ensureNotNull(tokens.toNonEmptySetOrNull()) { + Error.EmptyTokens + } + + if (isSortedByBalance) sortTokensByBalance(tokensNes) else tokensNes + } + } + + fun getSortType() = if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE + + private fun groupTokens(networks: NonEmptySet): NonEmptySet { + val groupedTokens = tokens + .groupBy { it.networkId } + .map { (networkId, tokens) -> + val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { + Error.NetworkNotFound(networkId) + } + + NetworkGroup( + networkId = network.id, + name = network.name, + tokens = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, + ) + } + .toNonEmptySetOrNull() + + return ensureNotNull(groupedTokens) { Error.EmptyTokens } + } + + private fun groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptySet { + val groupsWithSortedTokens = groupTokens(networks) + .map { group -> + val tokens = group.tokens as? NonEmptySet + ?: error("Tokens can not be empty here") + group.copy(tokens = sortTokensByBalance(tokens)) + } + .toNonEmptySet() + + return if (isAnyTokenLoading) { + groupsWithSortedTokens + } else { + sortGroupsByBalance(groupsWithSortedTokens) + } + } + + private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { + return if (isAnyTokenLoading) { + tokens + } else { + tokens.sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } + .toNonEmptySetOrNull() + ?: error("Tokens can not be empty here") + } + } + + private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { + return groupsWithSortedTokens + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } + } + .toNonEmptySetOrNull() + ?: error("Tokens can not be empty here") + } + + sealed class Error { + + object EmptyTokens : Error() + + object EmptyNetworks : Error() + + data class NetworkNotFound(val networkId: Network.ID) : Error() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt similarity index 84% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt index c0ebeeafa7..072b541737 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -1,9 +1,5 @@ -package com.tangem.domain.tokens.utils +package com.tangem.domain.tokens.operations -import arrow.core.raise.Raise -import arrow.core.raise.ensure -import arrow.core.raise.ensureNotNull -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token @@ -17,8 +13,7 @@ internal class TokenStatusOperations( private val quote: Quote?, private val networkStatus: NetworkStatus?, private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, -) : Raise by raise { +) { suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.single) { TokenStatus( @@ -68,7 +63,7 @@ internal class TokenStatusOperations( private fun getTokenAmount(): BigDecimal { val amount = networkStatus?.value?.amounts?.get(token.id) - return ensureNotNull(amount) { TokensError.TokenAmountNotFound(token.id) } + return amount ?: error("Incorrect network status: $networkStatus") } private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? { @@ -78,9 +73,6 @@ internal class TokenStatusOperations( } private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { - val fiatAmount = amount * fiatRate - ensure(condition = fiatAmount >= BigDecimal.ZERO) { TokensError.TokenFiatAmountLessThenZero(token.id) } - - return fiatAmount + return amount * fiatRate } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt similarity index 60% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt index 0c90a3c80b..e5dfd69fa0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt @@ -1,9 +1,10 @@ -package com.tangem.domain.tokens.utils +package com.tangem.domain.tokens.operations import arrow.core.NonEmptySet import arrow.core.raise.Raise import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -14,15 +15,33 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext @Suppress("LongParameterList") -internal class TokensStatusesOperations( +internal class TokensStatusesOperations( private val tokensRepository: TokensRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, private val refresh: Boolean, private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, -) : Raise by raise { + raise: Raise, + transformError: (Error) -> E, +) : DelegatedRaise(raise, transformError) { + + constructor( + userWalletId: UserWalletId, + refresh: Boolean, + useCase: GetTokenListUseCase, + raise: Raise, + transformError: (Error) -> E, + ) : this( + tokensRepository = useCase.tokensRepository, + quotesRepository = useCase.quotesRepository, + networksRepository = useCase.networksRepository, + userWalletId = userWalletId, + refresh = refresh, + dispatchers = useCase.dispatchers, + raise = raise, + transformError = transformError, + ) fun getTokensStatusesFlow(): Flow> { return getTokens().flatMapConcat { @@ -46,37 +65,43 @@ internal class TokensStatusesOperations( quotes: Set, networkStatuses: Set, ): Set = withContext(dispatchers.single) { - tokens.map { token -> + tokens.mapTo(hashSetOf()) { token -> val quote = quotes.firstOrNull { it.tokenId == token.id } val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } createStatus(token, quote, networkStatus) - }.toSet() + } } private suspend fun createStatus(token: Token, quote: Quote?, networkStatus: NetworkStatus?): TokenStatus { - val operations = TokenStatusOperations(token, quote, networkStatus, dispatchers, raise = this) - return operations.createTokenStatus() + val tokenStatusOperations = TokenStatusOperations( + token = token, + quote = quote, + networkStatus = networkStatus, + dispatchers = dispatchers, + ) + + return tokenStatusOperations.createTokenStatus() } private fun getTokens(): Flow> { return tokensRepository.getTokens(userWalletId, refresh) - .onEmpty { raise(TokensError.EmptyTokens) } - .map { it.bind() } + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyTokens) } .flowOn(dispatchers.io) } private fun getQuotes(tokensIds: NonEmptySet): Flow> { return quotesRepository.getQuotes(tokensIds, refresh) - .onEmpty { raise(TokensError.EmptyQuotes) } - .map { it.bind() } + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyQuotes) } .flowOn(dispatchers.io) } private fun getNetworksStatues(groupedTokens: Map>): Flow> { return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) - .onEmpty { raise(TokensError.EmptyNetworkStatues) } - .map { it.bind() } + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyNetworksStatuses) } .flowOn(dispatchers.io) } @@ -85,10 +110,21 @@ internal class TokensStatusesOperations( tokens .groupBy { it.networkId } .mapValues { (_, tokens) -> - tokens.toNonEmptySetOrNull() - ?.map { it.id } - ?.toNonEmptySet() - ?: raise(TokensError.EmptyTokens) + // Can not be empty + tokens.toNonEmptySetOrNull()!! + .map { it.id } + .toNonEmptySet() } } + + sealed class Error { + + object EmptyTokens : Error() + + object EmptyQuotes : Error() + + object EmptyNetworksStatuses : Error() + + data class DataError(val cause: Throwable) : Error() + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index df231dd283..93f7f7c95b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.repository -import arrow.core.Either -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Token @@ -16,11 +14,11 @@ import kotlinx.coroutines.flow.Flow * */ interface NetworksRepository { - fun getNetworks(networksIds: Set): Either> + fun getNetworks(networksIds: Set): Set fun getNetworkStatuses( userWalletId: UserWalletId, networks: Map>, refresh: Boolean, - ): Flow>> + ): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 314c1d1177..984ffb6608 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.repository -import arrow.core.Either -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token import kotlinx.coroutines.flow.Flow @@ -14,5 +12,5 @@ import kotlinx.coroutines.flow.Flow * */ interface QuotesRepository { - fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> + fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index 4b1ed804c3..b544ecb8f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.repository -import arrow.core.Either -import com.tangem.domain.tokens.error.TokensError import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -19,11 +17,11 @@ interface TokensRepository { sortedTokensIds: Set, isGrouped: Boolean, isSortedByBalance: Boolean, - ): Either + ) - fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> + fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> - fun isTokensGrouped(userWalletId: UserWalletId): Flow> + fun isTokensGrouped(userWalletId: UserWalletId): Flow - fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> + fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt deleted file mode 100644 index f5d8bf0d5c..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.tangem.domain.tokens.utils - -import arrow.core.NonEmptySet -import arrow.core.raise.Raise -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.tokens.error.TokensError -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.TokensRepository -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import java.math.BigDecimal - -@Suppress("LongParameterList") -internal class TokenListOperations( - private val tokensRepository: TokensRepository, - private val networksRepository: NetworksRepository, - private val userWalletId: UserWalletId, - private val totalFiatBalance: TokenList.FiatBalance, - private val isAnyTokenLoading: Boolean, - private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, -) : Raise by raise { - - fun getTokenListFlow(tokens: Set): Flow { - return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> - createTokenList(tokens, isGrouped, isSortedByBalance) - } - } - - private suspend fun createTokenList( - tokens: Set, - isGrouped: Boolean, - isSortedByBalance: Boolean, - ): TokenList = withContext(dispatchers.single) { - val tokensNes = tokens.toNonEmptySetOrNull() - - when { - tokensNes == null -> TokenList.NotInitialized - isGrouped -> { - val networks = getNetworks(tokensNes) - - createGroupedTokenList(tokensNes, networks, isSortedByBalance) - } - else -> createUngroupedTokenList(tokensNes, isSortedByBalance) - } - } - - private fun createUngroupedTokenList( - tokens: NonEmptySet, - isSortedByBalance: Boolean, - ): TokenList.Ungrouped { - return TokenList.Ungrouped( - sortedBy = getSortType(isSortedByBalance), - totalFiatBalance = totalFiatBalance, - tokens = if (isSortedByBalance) sortTokensByBalance(tokens) else tokens, - ) - } - - private fun createGroupedTokenList( - tokens: NonEmptySet, - networks: NonEmptySet, - isSortedByBalance: Boolean, - ): TokenList.GroupedByNetwork { - return TokenList.GroupedByNetwork( - sortedBy = getSortType(isSortedByBalance), - totalFiatBalance = totalFiatBalance, - groups = if (isSortedByBalance) groupAndSortTokens(tokens, networks) else groupTokens(tokens, networks), - ) - } - - private fun groupAndSortTokens( - tokens: NonEmptySet, - networks: NonEmptySet, - ): NonEmptySet { - val groupsWithSortedTokens = groupTokens(tokens, networks) - .map { group -> - group.copy(tokens = sortTokensByBalance(group.tokens as NonEmptySet)) - } - .toNonEmptySet() - val sortedGroups = if (isAnyTokenLoading) { - groupsWithSortedTokens - } else { - sortGroupsByBalance(groupsWithSortedTokens) - } - - return sortedGroups - } - - private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { - val sortedTokens = if (isAnyTokenLoading) { - tokens - } else { - tokens - .sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } - .toNonEmptySetOrNull() - } - - return sortedTokens!! - } - - private fun groupTokens( - tokens: NonEmptySet, - networks: NonEmptySet, - ): NonEmptySet { - val groups = tokens - .groupBy { it.networkId } - .map { (networkId, tokens) -> - val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { - TokensError.NetworkNotFound(networkId) - } - - NetworkGroup( - networkId = network.id, - name = network.name, - tokens = tokens.toNonEmptySetOrNull()!!, - ) - } - .toNonEmptySetOrNull() - - return groups!! - } - - private suspend fun getNetworks(tokens: NonEmptySet): NonEmptySet { - return withContext(dispatchers.io) { - val networksIds = tokens.map { it.networkId }.toNonEmptySet() - val networks = networksRepository.getNetworks(networksIds).bind() - ensureNotNull(networks.toNonEmptySetOrNull()) { TokensError.EmptyNetworks } - } - } - - private fun getIsGrouped(): Flow { - return tokensRepository.isTokensGrouped(userWalletId) - .map { it.bind() } - .flowOn(dispatchers.io) - } - - private fun getIsSortedByBalance(): Flow { - return tokensRepository.isTokensSortedByBalance(userWalletId) - .map { it.bind() } - .flowOn(dispatchers.io) - } - - private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { - return groupsWithSortedTokens - .sortedByDescending { group -> - group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } - } - .toNonEmptySetOrNull()!! - } - - private fun getSortType(isSortedByBalance: Boolean) = - if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index e876c1924d..fb26a27895 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -3,7 +3,8 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokenLists @@ -18,12 +19,10 @@ import com.tangem.domain.tokens.repository.MockTokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) internal class GetTokenListUseCaseTest { private val dispatchers = TestingCoroutineDispatcherProvider() @@ -49,9 +48,9 @@ internal class GetTokenListUseCaseTest { @Test fun `when tokens getting failed then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyTokens.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - val useCase = getUseCase(tokens = flowOf(expectedResult)) + val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When val result = useCase(userWalletId).first() @@ -63,9 +62,9 @@ internal class GetTokenListUseCaseTest { @Test fun `when quotes getting failed then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyQuotes.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - val useCase = getUseCase(quotes = flowOf(expectedResult)) + val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When val result = useCase(userWalletId).first() @@ -77,10 +76,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks getting failed and list is groped then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyNetworkStatues.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase( - networks = expectedResult, + networks = DataError.NetworkError.NoInternetConnection.left(), isGrouped = flowOf(true.right()), ) @@ -94,9 +93,9 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks statuses getting failed then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyNetworkStatues.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - val useCase = getUseCase(statuses = flowOf(expectedResult)) + val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When val result = useCase(userWalletId).first() @@ -108,9 +107,9 @@ internal class GetTokenListUseCaseTest { @Test fun `when grouping type getting failed then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyTokens.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - val useCase = getUseCase(isGrouped = flowOf(expectedResult)) + val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When val result = useCase(userWalletId).first() @@ -122,9 +121,9 @@ internal class GetTokenListUseCaseTest { @Test fun `when sorting type getting failed then error should be received`() = runTest { // Given - val expectedResult = TokensError.EmptyTokens.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - val useCase = getUseCase(isSortedByBalance = flowOf(expectedResult)) + val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When val result = useCase(userWalletId).first() @@ -136,10 +135,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when tokens getting failed on second emit then error should be received`() = runTest { // Given - val error = TokensError.EmptyTokens.left() + val error = DataError.NetworkError.NoInternetConnection.left() val expectedResult = listOf( MockTokenLists.ungroupedTokenList.right(), - error, + TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(), ) val useCase = getUseCase( @@ -173,10 +172,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when list is grouped and networks getting failed then error should be received`() = runTest { - val expectedResult = TokensError.EmptyNetworks.left() + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase( - networks = expectedResult, + networks = DataError.NetworkError.NoInternetConnection.left(), isGrouped = flowOf(true.right()), ) @@ -235,8 +234,8 @@ internal class GetTokenListUseCaseTest { } @Test - fun `when networks is empty and list is grouped then error should be received`() = runTest { - val expectedResult = TokensError.EmptyNetworks.left() + fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { + val expectedResult = TokenListError.UnableToSortTokenList(MockTokenLists.ungroupedTokenList).left() val useCase = getUseCase( networks = emptySet().right(), @@ -252,7 +251,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when tokens flow is empty then error should be received`() = runTest { - val expectedResult = TokensError.EmptyTokens.left() + val expectedResult = TokenListError.EmptyTokens.left() val useCase = getUseCase(tokens = flowOf()) @@ -265,7 +264,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = TokensError.EmptyNetworkStatues.left() + val expectedResult = TokenListError.EmptyTokens.left() val useCase = getUseCase(statuses = flowOf()) @@ -291,7 +290,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when quotes flow is empty then error should be received`() = runTest { - val expectedResult = TokensError.EmptyQuotes.left() + val expectedResult = TokenListError.EmptyTokens.left() val useCase = getUseCase(quotes = flowOf()) @@ -319,12 +318,12 @@ internal class GetTokenListUseCaseTest { } private fun getUseCase( - tokens: Flow>> = flowOf(MockTokens.tokens.right()), - quotes: Flow>> = flowOf(MockQuotes.quotes.right()), - networks: Either> = MockNetworks.networks.right(), - statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), - isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), - isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + quotes: Flow>> = flowOf(MockQuotes.quotes.right()), + networks: Either> = MockNetworks.networks.right(), + statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), + isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), + isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), ) = GetTokenListUseCase( dispatchers = dispatchers, tokensRepository = MockTokensRepository(tokens, isGrouped, isSortedByBalance), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 3267a30ba2..98c7002327 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -1,27 +1,29 @@ package com.tangem.domain.tokens.repository import arrow.core.Either -import com.tangem.domain.tokens.error.TokensError +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map internal class MockNetworksRepository( - private val networks: Either>, - private val statuses: Flow>>, + private val networks: Either>, + private val statuses: Flow>>, ) : NetworksRepository { - override fun getNetworks(networksIds: Set): Either> { - return networks + override fun getNetworks(networksIds: Set): Set { + return networks.getOrElse { throw it } } override fun getNetworkStatuses( userWalletId: UserWalletId, networks: Map>, refresh: Boolean, - ): Flow>> { - return statuses + ): Flow> { + return statuses.map { it.getOrElse { e -> throw e } } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index e82b0c0c82..fcf52e44e4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -1,16 +1,18 @@ package com.tangem.domain.tokens.repository import arrow.core.Either -import com.tangem.domain.tokens.error.TokensError +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map internal class MockQuotesRepository( - private val quotes: Flow>>, + private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow>> { - return quotes + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + return quotes.map { it.getOrElse { e -> throw e } } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index 5a7a757179..813816c52a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -1,16 +1,17 @@ package com.tangem.domain.tokens.repository import arrow.core.Either -import arrow.core.right -import com.tangem.domain.tokens.error.TokensError +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map internal class MockTokensRepository( - private val tokens: Flow>>, - private val isGrouped: Flow>, - private val isSortedByBalance: Flow>, + private val tokens: Flow>>, + private val isGrouped: Flow>, + private val isSortedByBalance: Flow>, ) : TokensRepository { override suspend fun sortTokens( @@ -18,17 +19,17 @@ internal class MockTokensRepository( sortedTokensIds: Set, isGrouped: Boolean, isSortedByBalance: Boolean, - ): Either = Unit.right() + ) = Unit - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow>> { - return tokens + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return tokens.map { it.getOrElse { e -> throw e } } } - override fun isTokensGrouped(userWalletId: UserWalletId): Flow> { - return isGrouped + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return isGrouped.map { it.getOrElse { e -> throw e } } } - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow> { - return isSortedByBalance + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return isSortedByBalance.map { it.getOrElse { e -> throw e } } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt similarity index 56% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt index 0f8cb0ca2f..d6f3112be9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenErrorToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.utils -import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.utils.converter.Converter -internal class TokenErrorToWalletStateConverter( +internal class TokenListErrorToWalletStateConverter( private val currentState: WalletStateHolder, -) : Converter { +) : Converter { // TODO: [REDACTED_JIRA] - override fun convert(value: TokensError): WalletStateHolder { + override fun convert(value: TokenListError): WalletStateHolder { return currentState } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index d47726ddeb..c606c9e404 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -16,7 +16,7 @@ import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.tokens.error.TokensError +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -24,7 +24,7 @@ import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens -import com.tangem.feature.wallet.presentation.wallet.utils.TokenErrorToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -152,9 +152,9 @@ internal class WalletViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { - val updateStateWithError = { error: TokensError -> - val converter = TokenErrorToWalletStateConverter(uiState) + private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { + val updateStateWithError = { error: TokenListError -> + val converter = TokenListErrorToWalletStateConverter(uiState) converter.convert(error) } From 9e7e5f0157f2c692d02bb0af48047e77973e15d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 16:08:15 +0500 Subject: [PATCH 054/100] Updated on 2026-08-14 --- .../tokendetails/TokenDetailsPreviewData.kt | 14 ++++++++++++++ .../tokendetails/state/TokenDetailsState.kt | 3 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 2 ++ .../viewmodels/TokenDetailsViewModel.kt | 5 ++++- 4 files changed, 23 insertions(+), 1 deletion(-) 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 c94cca8569..5c20a6664d 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 @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -71,9 +73,21 @@ internal object TokenDetailsPreviewData { ) val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons) + val marketPriceContent = MarketPriceBlockState.Content( + currencyName = "USDT", + price = "98900 $", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "10.89%", + type = PriceChangeConfig.Type.UP, + ), + ) + + private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + val tokenDetailsState = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, + marketPriceBlockState = marketPriceLoading, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 988dd22bfa..8c7ba02c14 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,7 +1,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState + data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, + val marketPriceBlockState: MarketPriceBlockState, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index bd876c400d..a8a20dfa34 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -30,6 +31,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) { TokenInfoBlock(state = state.tokenInfoBlockState) TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) + MarketPriceBlock(state = state.marketPriceBlockState) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index bcbf76c58a..4a75d46fb5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -27,7 +27,10 @@ internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { // simulate loading state viewModelScope.launch { delay(LOADING_DELAY) - uiState = uiState.copy(tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent) + uiState = uiState.copy( + tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent, + marketPriceBlockState = TokenDetailsPreviewData.marketPriceContent, + ) } } From e2ce1e4a9708be853006f27999d8e1f140f60bfb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 21:59:22 +0800 Subject: [PATCH 055/100] Updated on 2026-08-14 --- .../components/notifications/Notification.kt | 65 +++++-- .../notifications/NotificationState.kt | 34 +++- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../presentation/common/WalletPreviewData.kt | 13 +- .../wallet/state/WalletBottomSheetConfig.kt | 79 +++++++++ .../wallet/state/WalletNotification.kt | 120 +++++++++++-- .../wallet/state/WalletStateHolder.kt | 5 + .../presentation/wallet/ui/WalletScreen.kt | 9 +- .../wallet/ui/components/WalletBottomSheet.kt | 161 ++++++++++++++++++ 9 files changed, 448 insertions(+), 39 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 9d4f2cf480..76b7894881 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -43,10 +45,10 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) { ) .clickable( enabled = when (state) { - is NotificationState.Simple -> false - is NotificationState.Action -> true + is NotificationState.Clickable -> true + is NotificationState.Simple, is NotificationState.Closable -> false }, - onClick = if (state is NotificationState.Action) { + onClick = if (state is NotificationState.Clickable) { state.onClick } else { {} @@ -67,12 +69,23 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) { ) NotificationInfoBlock( - title = state.title, - subtitle = state.subtitle, + title = state.title.resolveReference(), + subtitle = state.subtitle?.resolveReference(), modifier = Modifier.align(alignment = Alignment.CenterStart), ) - if (state is NotificationState.Action) { + if (state is NotificationState.Closable) { + Icon( + modifier = Modifier + .size(size = TangemTheme.dimens.size20) + .align(alignment = Alignment.TopEnd), + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + + if (state is NotificationState.Clickable) { Icon( modifier = Modifier .size(size = TangemTheme.dimens.size20) @@ -149,30 +162,50 @@ private fun Preview_WarningNotification_Dark( private class NotificationStateProvider : CollectionPreviewParameterProvider( collection = listOf( NotificationState.Simple( - title = "Your wallet hasn’t been backed up", - subtitle = "Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...", + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), iconResId = R.drawable.img_attention_20, ), NotificationState.Simple( - title = "Your wallet hasn’t been backed up", + title = TextReference.Str("Your wallet hasn’t been backed up"), subtitle = null, iconResId = R.drawable.ic_alert_circle_24, tint = TangemColorPalette.Amaranth, ), - NotificationState.Action( - title = "Your wallet hasn’t been backed up", - subtitle = "Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...", + NotificationState.Clickable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), iconResId = R.drawable.img_attention_20, onClick = {}, ), - NotificationState.Action( - title = "Your wallet hasn’t been backed up", + NotificationState.Clickable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), subtitle = null, iconResId = R.drawable.ic_alert_circle_24, tint = TangemColorPalette.Amaranth, onClick = {}, ), + NotificationState.Closable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = {}, + ), + NotificationState.Closable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = null, + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + onCloseClick = {}, + ), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt index 065fa1e125..f787e62a66 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference /** * Notification component state @@ -14,8 +15,8 @@ import androidx.compose.ui.graphics.Color [REDACTED_AUTHOR] */ sealed class NotificationState( - open val title: String, - open val subtitle: String? = null, + open val title: TextReference, + open val subtitle: TextReference? = null, @DrawableRes open val iconResId: Int, open val tint: Color? = null, ) { @@ -29,8 +30,8 @@ sealed class NotificationState( * @property tint icon tint */ data class Simple( - override val title: String, - override val subtitle: String? = null, + override val title: TextReference, + override val subtitle: TextReference? = null, @DrawableRes override val iconResId: Int, override val tint: Color? = null, ) : NotificationState(title, subtitle, iconResId, tint) @@ -42,13 +43,30 @@ sealed class NotificationState( * @property subtitle subtitle * @property iconResId icon resource id * @property tint icon tint - * @param onClick lambda be invoked when notification component is clicked + * @property onClick lambda be invoked when notification component is clicked */ - data class Action( - override val title: String, - override val subtitle: String? = null, + data class Clickable( + override val title: TextReference, + override val subtitle: TextReference? = null, @DrawableRes override val iconResId: Int, override val tint: Color? = null, val onClick: () -> Unit, ) : NotificationState(title, subtitle, iconResId, tint) + + /** + * Closable notification state + * + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property tint icon tint + * @property onCloseClick lambda be invoked when close button is clicked + */ + data class Closable( + override val title: TextReference, + override val subtitle: TextReference? = null, + @DrawableRes override val iconResId: Int, + override val tint: Color? = null, + val onCloseClick: (() -> Unit)? = null, + ) : NotificationState(title, subtitle, iconResId, tint) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index a78bd58dbd..8e17a060f2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -102,6 +102,7 @@ data class TangemDimens internal constructor( val spacing34: Dp = 34.dp, val spacing36: Dp = 34.dp, val spacing38: Dp = 38.dp, + val spacing40: Dp = 40.dp, val spacing44: Dp = 44.dp, val spacing50: Dp = 50.dp, val spacing52: Dp = 52.dp, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index bc2443bc0e..3e1d581e0c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -200,6 +200,15 @@ internal object WalletPreviewData { ), ) + val bottomSheet = WalletBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = {}, + onScanClick = {}, + ), + ) + private val manageButtons = persistentListOf( WalletManageButton.Buy(onClick = {}), WalletManageButton.Send(onClick = {}), @@ -268,9 +277,10 @@ internal object WalletPreviewData { notifications = persistentListOf( WalletNotification.UnreachableNetworks, WalletNotification.LikeTangemApp(onClick = {}), - WalletNotification.NeedToBackup(onClick = {}), + WalletNotification.BackupCard(onClick = {}), WalletNotification.ScanCard(onClick = {}), ), + bottomSheet = bottomSheet, onOrganizeTokensClick = {}, ) @@ -303,6 +313,7 @@ internal object WalletPreviewData { ), notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), buttons = manageButtons.map(WalletManageButton::config).toPersistentList(), + bottomSheet = bottomSheet, marketPriceBlockState = MarketPriceBlockState.Content( currencyName = "BTC", price = "98900.12$", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt new file mode 100644 index 0000000000..5aafcd4464 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.impl.R + +/** + * Wallet bottom sheet config + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property content content config + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize notification strings [REDACTED_JIRA] +internal data class WalletBottomSheetConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val content: BottomSheetContentConfig, +) { + + sealed class BottomSheetContentConfig( + open val title: TextReference, + open val subtitle: TextReference, + @DrawableRes open val iconResId: Int, + open val tint: Color? = null, + val primaryButtonConfig: ButtonConfig, + val secondaryButtonConfig: ButtonConfig? = null, + ) { + + data class ButtonConfig(val text: String, val onClick: () -> Unit, @DrawableRes val iconResId: Int? = null) + + data class UnlockWallets( + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : BottomSheetContentConfig( + title = TextReference.Str(value = "Unlock needed"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + + "incididunt ut labore et dolore magna aliqua.", + ), + iconResId = R.drawable.ic_locked_24, + tint = TangemColorPalette.Black, + primaryButtonConfig = ButtonConfig(text = "Unlock", onClick = onUnlockClick), + secondaryButtonConfig = ButtonConfig( + text = "Scan card", + onClick = onScanClick, + iconResId = R.drawable.ic_tangem_24, + ), + ) + + data class LikeTangemApp( + val onRateTheAppClick: () -> Unit, + val onShareClick: () -> Unit, + ) : BottomSheetContentConfig( + title = TextReference.Str(value = "Like Tangem App?"), + subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"), + iconResId = R.drawable.ic_star_24, + tint = TangemColorPalette.Tangerine, + primaryButtonConfig = ButtonConfig(text = "Rate the app", onClick = onRateTheAppClick), + secondaryButtonConfig = ButtonConfig(text = "Share feedback", onClick = onShareClick), + ) + + data class MultiWalletAlreadySignedHashes(val onLearnClick: () -> Unit) : BottomSheetContentConfig( + title = TextReference.Res( + id = R.string.warning_important_security_info, + formatArgs = WrappedList(listOf("\u26A0")), + ), + subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), + iconResId = R.drawable.img_attention_20, + tint = null, + primaryButtonConfig = ButtonConfig(text = "Learn more", onClick = onLearnClick), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt index 39e760dd4f..66079037c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.notifications.NotificationState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.feature.wallet.impl.R @@ -11,26 +13,107 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ +// TODO: Finalize notification strings [REDACTED_JIRA] sealed class WalletNotification(open val state: NotificationState) { + /** Clickable notification */ + sealed interface Clickable { + + /** Lambda be invoked when notification is clicked */ + val onClick: () -> Unit + } + + /** "Development card" notification */ + object DevCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_developer_card), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Test card" notification */ + object TestCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.warning_testnet_card_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Demo card" notification */ + object DemoCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_demo_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Card verification failed" notification */ + object CardVerificationFailed : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.warning_failed_to_verify_card_title), + subtitle = TextReference.Res(id = R.string.warning_failed_to_verify_card_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** + * "Wallet already signed hashes" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class WalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), + iconResId = R.drawable.img_attention_20, + onClick = onClick, + tint = null, + ), + ) + + /** + * "Multi wallet already signed hashes" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class MultiWalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Res( + id = R.string.warning_important_security_info, + formatArgs = WrappedList(listOf("\u26A0")), + ), + subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), + iconResId = R.drawable.img_attention_20, + onClick = onClick, + tint = null, + ), + ) + /** * "Backup the card" notification * * @property onClick lambda be invoked when notification is clicked */ - data class NeedToBackup(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Backup your card", - iconResId = R.drawable.ic_alert_circle_24, + data class BackupCard(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Backup your card"), + iconResId = R.drawable.img_attention_20, onClick = onClick, - tint = TangemColorPalette.Amaranth, + tint = null, ), ) /** "Unreachable networks" notification */ object UnreachableNetworks : WalletNotification( state = NotificationState.Simple( - title = "Some networks are unreachable", + title = TextReference.Str(value = "Some networks are unreachable"), iconResId = R.drawable.img_attention_20, tint = null, ), @@ -41,9 +124,9 @@ sealed class WalletNotification(open val state: NotificationState) { * * @property onClick lambda be invoked when notification is clicked */ - data class LikeTangemApp(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Like Tangem App?", + data class LikeTangemApp(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Like Tangem App?"), iconResId = R.drawable.ic_star_24, onClick = onClick, tint = TangemColorPalette.Tangerine, @@ -55,11 +138,24 @@ sealed class WalletNotification(open val state: NotificationState) { * * @property onClick lambda be invoked when notification is clicked */ - data class ScanCard(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Scan your card to continue", + data class ScanCard(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Scan your card to continue"), iconResId = R.drawable.ic_tangem_24, onClick = onClick, ), ) + + /** + * "Unlock wallets" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class UnlockWallets(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Unlock needed"), + iconResId = R.drawable.ic_locked_24, + onClick = onClick, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index c4b9c37ee8..66785bd0ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -23,6 +23,7 @@ internal sealed class WalletStateHolder( open val pullToRefreshConfig: WalletPullToRefreshConfig, open val contentItems: ImmutableList, open val notifications: ImmutableList, + open val bottomSheet: WalletBottomSheetConfig? = null, ) { /** @@ -43,6 +44,7 @@ internal sealed class WalletStateHolder( override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, + override val bottomSheet: WalletBottomSheetConfig, val onOrganizeTokensClick: () -> Unit, ) : WalletStateHolder( onBackClick = onBackClick, @@ -50,6 +52,7 @@ internal sealed class WalletStateHolder( walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, contentItems = contentItems, + bottomSheet = bottomSheet, notifications = notifications, ) @@ -72,6 +75,7 @@ internal sealed class WalletStateHolder( override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, + override val bottomSheet: WalletBottomSheetConfig, val buttons: ImmutableList, val marketPriceBlockState: MarketPriceBlockState, ) : WalletStateHolder( @@ -81,5 +85,6 @@ internal sealed class WalletStateHolder( pullToRefreshConfig = pullToRefreshConfig, contentItems = contentItems, notifications = notifications, + bottomSheet = bottomSheet, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 42f5485b26..5f825b106f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -13,16 +13,16 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton -import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.Transaction @@ -34,6 +34,7 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle @@ -159,6 +160,10 @@ internal fun WalletScreen(state: WalletStateHolder) { ) } } + + state.bottomSheet?.let { bottomSheetConfig -> + if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig) + } } @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt new file mode 100644 index 0000000000..45b7a7a752 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconStart +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig + +/** + * Wallet bottom sheet with detail notification information + * + * @param config component config + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletBottomSheet(config: WalletBottomSheetConfig) { + ModalBottomSheet( + onDismissRequest = config.onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = TangemTheme.colors.background.primary, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + BottomSheetContent(config = config.content) + } +} + +@Composable +private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetContentConfig) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val iconTint = config.tint + if (iconTint != null) { + Icon( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size48), + tint = iconTint, + ) + } else { + Image( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size48), + ) + } + + Text( + text = config.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + Text( + text = config.subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body2, + ) + + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { + val buttonModifier = Modifier.fillMaxWidth() + PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) + + if (config.secondaryButtonConfig != null) { + SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) + } + } + } +} + +@Composable +private fun PrimaryButton( + config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, + modifier: Modifier = Modifier, +) { + if (config.iconResId == null) { + PrimaryButton( + text = config.text, + onClick = config.onClick, + modifier = modifier, + ) + } else { + PrimaryButtonIconStart( + text = config.text, + iconResId = config.iconResId, + onClick = config.onClick, + modifier = modifier, + ) + } +} + +@Composable +private fun SecondaryButton( + config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, + modifier: Modifier = Modifier, +) { + if (config.iconResId == null) { + SecondaryButton( + text = config.text, + onClick = config.onClick, + modifier = modifier, + ) + } else { + SecondaryButtonIconStart( + text = config.text, + iconResId = config.iconResId, + onClick = config.onClick, + modifier = modifier, + ) + } +} + +@Preview +@Composable +private fun WalletBottomSheetContent_Light( + @PreviewParameter(WalletBottomSheetConfigProvider::class) + config: WalletBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + BottomSheetContent(config = config.content) + } +} + +@Preview +@Composable +private fun WalletBottomSheetContent_Dark( + @PreviewParameter(WalletBottomSheetConfigProvider::class) + config: WalletBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + BottomSheetContent(config = config.content) + } +} + +private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( + collection = listOf(WalletPreviewData.bottomSheet), +) \ No newline at end of file From 584d99e95e11518fa9ef509e36b9a339d0150472 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 14:18:24 +0300 Subject: [PATCH 056/100] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 12 +++- .../coroutines/CoroutineDispatcherProvider.kt | 3 + .../domain/tokens/GetTokenListUseCase.kt | 5 +- .../tokens/ToggleTokenListSortingUseCase.kt | 66 +++++++++++++++++++ .../domain/tokens/error/TokenListError.kt | 19 ------ .../tokens/error/TokenListSortingError.kt | 10 +++ .../error/mapper/TokenListErrorMappers.kt | 25 +++++++ .../mapper/TokenListSortingErrorMappers.kt | 13 ++++ .../domain/tokens/model/NetworkGroup.kt | 3 +- .../tokens/operations/TokenListOperations.kt | 4 +- .../operations/TokenListSortingOperations.kt | 35 +++++++--- .../operations/TokenStatusOperations.kt | 2 +- .../operations/TokensStatusesOperations.kt | 4 +- .../domain/tokens/mock/MockTokenLists.kt | 11 ++-- .../utils/TokenListToContentItemsConverter.kt | 2 +- 15 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index bda1a7734d..a43ebfeb6c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -1,6 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository @@ -22,5 +22,13 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ) = GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + ): GetTokenListUseCase { + return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase(dispatchers) + } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt index 6f28c0350e..f15bce01ea 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -9,18 +9,21 @@ import javax.inject.Inject interface CoroutineDispatcherProvider { val main: CoroutineDispatcher val io: CoroutineDispatcher + val default: CoroutineDispatcher val single: CoroutineDispatcher } class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { override val main: CoroutineDispatcher = Dispatchers.Main override val io: CoroutineDispatcher = Dispatchers.IO + override val default: CoroutineDispatcher = Dispatchers.Default override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() } class TestingCoroutineDispatcherProvider( override val main: CoroutineDispatcher = Dispatchers.Unconfined, override val io: CoroutineDispatcher = Dispatchers.Unconfined, + override val default: CoroutineDispatcher = Dispatchers.Unconfined, override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(), ) : CoroutineDispatcherProvider diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 3f42be91fc..fcd8578f39 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import arrow.core.right import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus import com.tangem.domain.tokens.operations.TokenListOperations @@ -55,7 +56,7 @@ class GetTokenListUseCase( refresh = refresh, useCase = this@GetTokenListUseCase, raise = this, - transformError = TokenListError::fromTokenStatusesOperations, + transformError = TokensStatusesOperations.Error::mapToTokenListError, ) return operations.getTokensStatusesFlow() @@ -70,7 +71,7 @@ class GetTokenListUseCase( tokens = tokens, useCase = this@GetTokenListUseCase, raise = this, - transform = TokenListError::fromTokenListOperations, + transform = TokenListOperations.Error::mapToTokenListError, ) return operations.getTokenListFlow() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt new file mode 100644 index 0000000000..4b0b8664f4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.TokenListSortingOperations +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class ToggleTokenListSortingUseCase( + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke(tokenList: TokenList): Either { + return withContext(dispatchers.default) { + either { + ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + TokenListSortingError.TokenListIsLoading + } + + when (tokenList) { + is TokenList.GroupedByNetwork -> sortGroupedTokenList(tokenList) + is TokenList.Ungrouped -> sortUngroupedTokenList(tokenList) + is TokenList.NotInitialized -> raise(TokenListSortingError.TokenListIsEmpty) + } + } + } + } + + private suspend fun Raise.sortGroupedTokenList( + tokenList: TokenList.GroupedByNetwork, + ): TokenList.GroupedByNetwork { + val operations = getSortingOperations(tokenList) + val networks = tokenList.groups.map { it.network }.toSet() + + return tokenList.copy( + groups = operations.getGroupedTokens(networks), + sortedBy = operations.getSortType(), + ) + } + + private suspend fun Raise.sortUngroupedTokenList( + tokenList: TokenList.Ungrouped, + ): TokenList.Ungrouped { + val operations = getSortingOperations(tokenList) + + return tokenList.copy( + tokens = operations.getTokens(), + sortedBy = operations.getSortType(), + ) + } + + private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { + return TokenListSortingOperations( + tokenList = tokenList, + sortByBalance = tokenList.sortedBy != TokenList.SortType.BALANCE, + dispatchers = dispatchers, + raise = this, + transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt index 06897839f6..0a321a7874 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -1,8 +1,6 @@ package com.tangem.domain.tokens.error import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.operations.TokensStatusesOperations sealed class TokenListError { @@ -11,21 +9,4 @@ sealed class TokenListError { data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : TokenListError() data class DataError(val cause: Throwable) : TokenListError() - - internal companion object { - - fun fromTokenStatusesOperations(error: TokensStatusesOperations.Error): TokenListError = when (error) { - is TokensStatusesOperations.Error.DataError -> DataError(error.cause) - is TokensStatusesOperations.Error.EmptyNetworksStatuses, - is TokensStatusesOperations.Error.EmptyQuotes, - is TokensStatusesOperations.Error.EmptyTokens, - -> EmptyTokens - } - - fun fromTokenListOperations(error: TokenListOperations.Error): TokenListError = when (error) { - is TokenListOperations.Error.DataError -> DataError(error.cause) - is TokenListOperations.Error.UnableToSortTokenList -> UnableToSortTokenList(error.unsortedTokenList) - is TokenListOperations.Error.UnableToGroupTokenList -> UnableToSortTokenList(error.ungroupedTokenList) - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt new file mode 100644 index 0000000000..ae49dfb209 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.tokens.error + +sealed class TokenListSortingError { + + object TokenListIsLoading : TokenListSortingError() + + object TokenListIsEmpty : TokenListSortingError() + + object UnableToSortTokenList : TokenListSortingError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt new file mode 100644 index 0000000000..2d0798a489 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.operations.TokensStatusesOperations + +internal fun TokensStatusesOperations.Error.mapToTokenListError(): TokenListError { + return when (this) { + is TokensStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause) + is TokensStatusesOperations.Error.EmptyNetworksStatuses, + is TokensStatusesOperations.Error.EmptyQuotes, + is TokensStatusesOperations.Error.EmptyTokens, + -> TokenListError.EmptyTokens + } +} + +internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { + return when (this) { + is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) + is TokenListOperations.Error.UnableToSortTokenList -> + TokenListError.UnableToSortTokenList(this.unsortedTokenList) + is TokenListOperations.Error.UnableToGroupTokenList -> + TokenListError.UnableToSortTokenList(this.ungroupedTokenList) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt new file mode 100644 index 0000000000..2b492ad7ee --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.operations.TokenListSortingOperations + +internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError { + return when (this) { + is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty + is TokenListSortingOperations.Error.EmptyNetworks, + is TokenListSortingOperations.Error.NetworkNotFound, + -> TokenListSortingError.UnableToSortTokenList + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index acc05ff374..d1cc917d38 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model data class NetworkGroup( - val networkId: Network.ID, - val name: String, + val network: Network, val tokens: Set, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 87253155db..9283eacb78 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -51,7 +51,7 @@ internal class TokenListOperations( } private suspend fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { - return withContext(dispatchers.single) { + return withContext(dispatchers.default) { val tokensNes = tokens.toNonEmptySetOrNull() ?: return@withContext TokenList.NotInitialized @@ -78,7 +78,7 @@ internal class TokenListOperations( val sortingOperations = TokenListSortingOperations( tokens = tokens, isAnyTokenLoading = isAnyTokenLoading, - isSortedByBalance = isSortedByBalance, + sortByBalance = isSortedByBalance, dispatchers = dispatchers, raise = this, transformError = { e -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 5d1b4516bb..e40520ec97 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -17,20 +17,40 @@ import java.math.BigDecimal internal class TokenListSortingOperations( private val tokens: Set, private val isAnyTokenLoading: Boolean, - private val isSortedByBalance: Boolean, + private val sortByBalance: Boolean, private val dispatchers: CoroutineDispatcherProvider, raise: Raise, transformError: (Error) -> E, ) : DelegatedRaise(raise, transformError) { + constructor( + tokenList: TokenList, + dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> E, + sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, + isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading, + ) : this( + tokens = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.tokens }.toSet() + is TokenList.Ungrouped -> tokenList.tokens + is TokenList.NotInitialized -> emptySet() + }, + isAnyTokenLoading = isAnyTokenLoading, + sortByBalance = sortByBalance, + dispatchers = dispatchers, + raise = raise, + transformError = transformError, + ) + suspend fun getGroupedTokens(networks: Set): NonEmptySet { - return withContext(dispatchers.single) { + return withContext(dispatchers.default) { ensure(tokens.isNotEmpty()) { Error.EmptyTokens } val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { Error.EmptyNetworks } - if (isSortedByBalance) { + if (sortByBalance) { groupAndSortTokensByBalance(networksNes) } else { groupTokens(networksNes) @@ -39,16 +59,16 @@ internal class TokenListSortingOperations( } suspend fun getTokens(): NonEmptySet { - return withContext(dispatchers.single) { + return withContext(dispatchers.default) { val tokensNes = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens } - if (isSortedByBalance) sortTokensByBalance(tokensNes) else tokensNes + if (sortByBalance) sortTokensByBalance(tokensNes) else tokensNes } } - fun getSortType() = if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE + fun getSortType() = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE private fun groupTokens(networks: NonEmptySet): NonEmptySet { val groupedTokens = tokens @@ -59,8 +79,7 @@ internal class TokenListSortingOperations( } NetworkGroup( - networkId = network.id, - name = network.name, + network = network, tokens = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt index 072b541737..04478c7917 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -15,7 +15,7 @@ internal class TokenStatusOperations( private val dispatchers: CoroutineDispatcherProvider, ) { - suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.single) { + suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.default) { TokenStatus( id = token.id, networkId = token.networkId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt index e5dfd69fa0..cbe9d303ad 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt @@ -64,7 +64,7 @@ internal class TokensStatusesOperations( tokens: Set, quotes: Set, networkStatuses: Set, - ): Set = withContext(dispatchers.single) { + ): Set = withContext(dispatchers.default) { tokens.mapTo(hashSetOf()) { token -> val quote = quotes.firstOrNull { it.tokenId == token.id } val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } @@ -106,7 +106,7 @@ internal class TokensStatusesOperations( } private suspend fun groupTokens(tokens: NonEmptySet): Map> = - withContext(dispatchers.single) { + withContext(dispatchers.default) { tokens .groupBy { it.networkId } .mapValues { (_, tokens) -> diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index c705c614f2..b2befbeeb1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -14,24 +14,21 @@ internal object MockTokenLists { const val isSortedByBalance = false val networkGroup1 = NetworkGroup( - networkId = MockNetworks.network1.id, - name = MockNetworks.network1.name, + network = MockNetworks.network1, tokens = MockTokensStates.tokenStates .filter { it.networkId == MockNetworks.network1.id } .toNonEmptySetOrNull()!!, ) val networkGroup2 = NetworkGroup( - networkId = MockNetworks.network2.id, - name = MockNetworks.network2.name, + network = MockNetworks.network2, tokens = MockTokensStates.tokenStates .filter { it.networkId == MockNetworks.network2.id } .toNonEmptySetOrNull()!!, ) val networkGroup3 = NetworkGroup( - networkId = MockNetworks.network3.id, - name = MockNetworks.network3.name, + network = MockNetworks.network3, tokens = MockTokensStates.tokenStates .filter { it.networkId == MockNetworks.network3.id } .toNonEmptySetOrNull()!!, @@ -42,7 +39,7 @@ internal object MockTokenLists { val sortedNetworksGroups = networksGroups.map { group -> group.copy( tokens = MockTokensStates.loadedTokensStates - .filter { it.networkId == group.networkId } + .filter { it.networkId == group.network.id } .sortedByDescending { it.value.fiatAmount } .toNonEmptySetOrNull()!!, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index 731c766675..e488aa7b7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -44,7 +44,7 @@ internal class TokenListToContentItemsConverter( } private fun MutableList.addGroup(group: NetworkGroup): List { - this.add(MultiCurrencyItem.NetworkGroupTitle(group.name)) + this.add(MultiCurrencyItem.NetworkGroupTitle(group.network.name)) group.tokens.forEach { token -> this.addToken(token) From a57d077863883addf03228cbbd8c9fc45535c1da Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jul 2023 20:57:39 +0800 Subject: [PATCH 057/100] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 8 +- .../router/DefaultWalletRouter.kt | 6 +- .../wallet/state/WalletCardState.kt | 11 +- .../wallet/state/WalletStateHolder.kt | 107 ++++++++++++++- .../state/builder/WalletStateFactory.kt | 109 +++++++++++++++ .../wallet/ui/components/WalletsList.kt | 6 +- .../WalletContentItemDecoration.kt | 16 ++- .../utils/TokenListToWalletStateConverter.kt | 3 + .../wallet/viewmodels/WalletViewModel.kt | 128 ++++++++++-------- 9 files changed, 313 insertions(+), 81 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 3e1d581e0c..5dfb53248c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -20,7 +20,7 @@ internal object WalletPreviewData { val walletTopBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}) val walletCardContentState = WalletCardState.Content( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", balance = "8923,05 $", additionalInfo = "3 cards • Seed enabled", @@ -29,7 +29,7 @@ internal object WalletPreviewData { ) val walletCardLoadingState = WalletCardState.Loading( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, @@ -37,7 +37,7 @@ internal object WalletPreviewData { ) val walletCardHiddenContentState = WalletCardState.HiddenContent( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, @@ -45,7 +45,7 @@ internal object WalletPreviewData { ) val walletCardErrorState = WalletCardState.Error( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2406ada436..2f30c3d355 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.hilt.navigation.compose.hiltViewModel @@ -40,8 +41,9 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation startDestination = WalletScreens.WALLET.name, ) { composable(WalletScreens.WALLET.name) { - val viewModel = hiltViewModel() - .apply { router = this@DefaultWalletRouter } + val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) + WalletScreen(state = viewModel.uiState) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt index a04b0e3f45..b1f88360b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt @@ -2,13 +2,14 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.domain.wallets.models.UserWalletId /** Wallet card state */ @Immutable internal sealed interface WalletCardState { /** Id */ - val id: String + val id: UserWalletId /** Title */ val title: String @@ -34,7 +35,7 @@ internal sealed interface WalletCardState { * @property balance wallet balance */ data class Content( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -52,7 +53,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class Loading( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -69,7 +70,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class HiddenContent( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -86,7 +87,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class Error( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index 66785bd0ee..169139dacd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -2,7 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** * Wallet screen state holder @@ -26,6 +28,44 @@ internal sealed class WalletStateHolder( open val bottomSheet: WalletBottomSheetConfig? = null, ) { + fun copySealed( + onBackClick: () -> Unit = this.onBackClick, + topBarConfig: WalletTopBarConfig = this.topBarConfig, + walletsListConfig: WalletsListConfig = this.walletsListConfig, + pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, + contentItems: ImmutableList = this.contentItems, + notifications: ImmutableList = this.notifications, + bottomSheet: WalletBottomSheetConfig? = this.bottomSheet, + ): WalletStateHolder { + return when (this) { + is MultiCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems as ImmutableList, + notifications = notifications, + bottomSheet = bottomSheet, + ) + is SingleCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems as ImmutableList, + notifications = notifications, + bottomSheet = bottomSheet, + ) + is UnlockWalletContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + ) + is Loading -> copy(onBackClick = onBackClick) + } + } + /** * Multi currency wallet content state * @@ -44,7 +84,7 @@ internal sealed class WalletStateHolder( override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, - override val bottomSheet: WalletBottomSheetConfig, + override val bottomSheet: WalletBottomSheetConfig? = null, val onOrganizeTokensClick: () -> Unit, ) : WalletStateHolder( onBackClick = onBackClick, @@ -75,7 +115,7 @@ internal sealed class WalletStateHolder( override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, - override val bottomSheet: WalletBottomSheetConfig, + override val bottomSheet: WalletBottomSheetConfig? = null, val buttons: ImmutableList, val marketPriceBlockState: MarketPriceBlockState, ) : WalletStateHolder( @@ -87,4 +127,67 @@ internal sealed class WalletStateHolder( notifications = notifications, bottomSheet = bottomSheet, ) + + /** + * Unlock wallet content state + * + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked + * @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed + * @property onUnlockClick lambda be invoked when unlock button is clicked + * @property onScanClick lambda be invoked when scan card button is clicked + */ + data class UnlockWalletContent( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + val onUnlockWalletsNotificationClick: () -> Unit, + val onBottomSheetDismissRequest: () -> Unit, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = persistentListOf(WalletContentItemState.Loading), + notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)), + bottomSheet = WalletBottomSheetConfig( + isShow = false, + onDismissRequest = onBottomSheetDismissRequest, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ), + ) + + /** + * Loading state + * + * @property onBackClick lambda be invoked when back button is clicked + */ + data class Loading(override val onBackClick: () -> Unit) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}), + walletsListConfig = WalletsListConfig( + selectedWalletIndex = 0, + wallets = persistentListOf( + WalletCardState.Loading( + id = UserWalletId(stringValue = ""), + title = "", + additionalInfo = "", + imageResId = null, + ), + ), + onWalletChange = {}, + ), + pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}), + contentItems = persistentListOf(WalletContentItemState.Loading), + notifications = persistentListOf(), + bottomSheet = null, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt new file mode 100644 index 0000000000..21bf52bee0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt @@ -0,0 +1,109 @@ +package com.tangem.feature.wallet.presentation.wallet.state.builder + +import com.tangem.common.Provider +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.* +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class WalletStateFactory( + private val routerProvider: Provider, + private val onScanCardClick: () -> Unit, + private val onWalletChange: (Int) -> Unit, + private val onRefreshSwipe: () -> Unit, +) { + + fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick) + + fun getContentState(wallets: List): WalletStateHolder { + val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver + + return if (cardTypeResolver.isMultiwalletAllowed()) { + createMultiCurrencyState(wallets) + } else { + createSingleCurrencyState(wallets) + } + } + + private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { + return WalletStateHolder.MultiCurrencyContent( + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + contentItems = persistentListOf(), + notifications = persistentListOf(), // TODO: create notifications + bottomSheet = WalletBottomSheetConfig( + // TODO: check notifications + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = {}, + onShareClick = {}, + ), + ), + onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen, + ) + } + + private fun createSingleCurrencyState(wallets: List): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + contentItems = persistentListOf(), + notifications = persistentListOf(), // TODO: create notifications + bottomSheet = WalletBottomSheetConfig( + // TODO: check notifications + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = {}, + onShareClick = {}, + ), + ), + buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons + // TODO: create market price block + marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState, + ) + } + + private fun onBackClick() = routerProvider().popBackStack() + + private fun createTopBarConfig(): WalletTopBarConfig { + return WalletTopBarConfig( + onScanCardClick = onScanCardClick, + onMoreClick = routerProvider()::openDetailsScreen, + ) + } + + private fun createWalletsListConfig(wallets: List): WalletsListConfig { + return WalletsListConfig( + selectedWalletIndex = 0, + wallets = wallets.map { wallet -> + WalletCardState.Loading( + id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO + UserWalletId("123") + } else { + UserWalletId("321") + }, + // TODO: wallet.walletId, + title = wallet.name, + additionalInfo = "", // TODO + imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver), + ) + }.toImmutableList(), + onWalletChange = onWalletChange, + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 6c39b86f10..3f162e41f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -19,10 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector -import kotlinx.coroutines.InternalCoroutinesApi /** * Wallets list component @@ -32,7 +30,7 @@ import kotlinx.coroutines.InternalCoroutinesApi * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalFoundationApi::class, InternalCoroutinesApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) { val horizontalCardPadding = TangemTheme.dimens.spacing16 @@ -45,7 +43,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), ) { - items(items = config.wallets, key = WalletCardState::id) { state -> + items(items = config.wallets, key = { it.id.stringValue }) { state -> WalletCard(state = state, modifier = Modifier.width(itemWidth)) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt index ea04e144c4..b06ad40f02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt @@ -12,21 +12,27 @@ import com.tangem.core.ui.res.TangemTheme */ internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) - when (currentIndex) { - 0 -> { + val isSingleItem = currentIndex == 0 && lastIndex == 0 + when { + isSingleItem -> { + modifierWithHorizontalPadding + .padding(top = TangemTheme.dimens.spacing14) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + } + currentIndex == 0 -> { modifierWithHorizontalPadding .padding(top = TangemTheme.dimens.spacing14) .clip( - RoundedCornerShape( + shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, topEnd = TangemTheme.dimens.radius16, ), ) } - lastIndex -> { + currentIndex == lastIndex -> { modifierWithHorizontalPadding .clip( - RoundedCornerShape( + shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, bottomEnd = TangemTheme.dimens.radius16, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index af7cf9373e..1db861142b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -19,6 +19,9 @@ internal class TokenListToWalletStateConverter( return when (currentState) { is MultiCurrencyContent -> currentState.updateWithTokenList(value) is SingleCurrencyContent -> currentState.updateWithTokenList(value) + is WalletStateHolder.Loading, + is WalletStateHolder.UnlockWalletContent, + -> currentState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index c606c9e404..a369895e5b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -1,35 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import arrow.core.Either +import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.core.ui.components.transactions.TransactionState import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase import com.tangem.domain.card.GetBiometricsStatusUseCase import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig -import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.builder.WalletStateFactory import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -48,50 +46,66 @@ internal class WalletViewModel @Inject constructor( private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, + private val getWalletsUseCase: GetWalletsUseCase, private val scanCardProcessor: ScanCardProcessor, private val dispatchers: CoroutineDispatcherProvider, -) : ViewModel() { +) : ViewModel(), DefaultLifecycleObserver { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() + private val stateFactory = WalletStateFactory( + routerProvider = Provider { router }, + onScanCardClick = ::onScanCardClick, + onWalletChange = ::changeWallet, + onRefreshSwipe = ::refreshContent, + ) + /** Screen state */ - var uiState by mutableStateOf(getInitialState()) + var uiState by mutableStateOf(stateFactory.getInitialState()) private set - private var getTokenListJob: Job? = null - set(value) { - field?.cancel() - field = value + override fun onCreate(owner: LifecycleOwner) { + getWalletsUseCase() + .distinctUntilChanged() + .flowWithLifecycle(owner.lifecycle) + .onEach { wallets -> + if (wallets.isEmpty()) return@onEach + + uiState = stateFactory.getContentState(wallets = wallets) + updateContentItems() + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun updateContentItems() { + getTokenListUseCase( + userWalletId = uiState.walletsListConfig.wallets.get( + index = uiState.walletsListConfig.selectedWalletIndex, + ).id, + ) + .distinctUntilChanged() + .mapLatest(::updateStateWithTokenListOrError) + .onEach { + uiState = it.copySealed( + pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + private fun getRefreshingStatus(): Boolean { + return uiState.contentItems.any { state -> + val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token + val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction + + isMultiCurrencyItem?.state is TokenItemState.Loading || + isSingleCurrencyItem?.state is TransactionState.Loading || + state is WalletContentItemState.Loading } - - // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun getInitialState(): WalletStateHolder { - val state = WalletPreviewData.multicurrencyWalletScreenState.copy( - onBackClick = ::onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy( - onWalletChange = ::selectWallet, - ), - contentItems = getLoadingMultiCurrencyTokens(), - ) - - val selectedWalletIndex = state.walletsListConfig.selectedWalletIndex - val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(selectedWalletIndex) - launchGetTokenListJob(selectedWalletId) - - return state - } - - private fun onBackClick() { - router.popBackStack() - } - - private fun createTopBarConfig(): WalletTopBarConfig { - return WalletTopBarConfig( - onScanCardClick = ::onScanCardClick, - onMoreClick = { router.openDetailsScreen() }, - ) } private fun onScanCardClick() { @@ -124,12 +138,9 @@ internal class WalletViewModel @Inject constructor( } } - // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun selectWallet(index: Int) { + private fun changeWallet(index: Int) { if (uiState.walletsListConfig.selectedWalletIndex == index) return - Log.i("WalletViewModel", "selectWallet: $index") - uiState = when (val state = uiState) { is WalletStateHolder.MultiCurrencyContent -> state.copy( walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), @@ -137,19 +148,18 @@ internal class WalletViewModel @Inject constructor( is WalletStateHolder.SingleCurrencyContent -> state.copy( walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), ) + is WalletStateHolder.UnlockWalletContent -> state.copy( + walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), + ) + is WalletStateHolder.Loading -> state } - val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(index) - launchGetTokenListJob(selectedWalletId) + updateContentItems() } - private fun launchGetTokenListJob(userWalletId: UserWalletId) { - getTokenListJob = getTokenListUseCase(userWalletId) - .distinctUntilChanged() - .mapLatest(::updateStateWithTokenListOrError) - .onEach { uiState = it } - .flowOn(Dispatchers.Default) - .launchIn(viewModelScope) + private fun refreshContent() { + uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true)) + updateContentItems() } private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { From 11f475954468fe653303456678d641690d1f174f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jul 2023 20:00:23 +0300 Subject: [PATCH 058/100] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../datasource/di/CacheKeysStoreModule.kt | 20 +++++++ .../datasource/local/cache/CacheKeysStore.kt | 14 +++++ .../local/cache/RuntimeCacheKeysStore.kt | 25 ++++++++ .../datasource/local/cache/model/CacheKey.kt | 10 ++++ .../datasource/local/store/RuntimeStore.kt | 59 +++++++++++++++++++ data/common/.gitignore | 1 + data/common/build.gradle.kts | 16 +++++ data/common/src/main/AndroidManifest.xml | 1 + .../tangem/data/common/cache/CacheRegistry.kt | 52 ++++++++++++++++ .../data/common/cache/DefaultCacheRegistry.kt | 52 ++++++++++++++++ .../common/cache/di/CacheRegistryModule.kt | 21 +++++++ settings.gradle.kts | 1 + 13 files changed, 273 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt create mode 100644 data/common/.gitignore create mode 100644 data/common/build.gradle.kts create mode 100644 data/common/src/main/AndroidManifest.xml create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75db227f9d..8b1db8e62c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(project(":data:source:preferences")) implementation(projects.data.card) implementation(projects.data.tokens) + implementation(projects.data.common) /** Features */ implementation(project(":features:onboarding")) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt new file mode 100644 index 0000000000..ae96179015 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.cache.CacheKeysStore +import com.tangem.datasource.local.cache.RuntimeCacheKeysStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CacheKeysStoreModule { + + @Provides + @Singleton + fun provideCacheKeysStore(): CacheKeysStore { + return RuntimeCacheKeysStore() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt new file mode 100644 index 0000000000..fc32a2b16e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey + +interface CacheKeysStore { + + fun get(id: String): CacheKey? + + fun addOrReplace(key: CacheKey) + + fun remove(id: String) + + fun clear() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt new file mode 100644 index 0000000000..229b5ab30c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.datasource.local.store.RuntimeStore + +internal class RuntimeCacheKeysStore : CacheKeysStore { + + private val store = RuntimeStore(keyProvider = CacheKey::id) + + override fun get(id: String): CacheKey? { + return store.getSync { it.id == id }.firstOrNull() + } + + override fun addOrReplace(key: CacheKey) { + store.addOrReplace(key) + } + + override fun remove(id: String) { + store.remove { it.id == id } + } + + override fun clear() { + store.clear() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt new file mode 100644 index 0000000000..733e61e2b0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.cache.model + +import org.joda.time.Duration +import org.joda.time.LocalDateTime + +data class CacheKey( + val id: String, + val updatedAt: LocalDateTime, + val expiresIn: Duration, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt new file mode 100644 index 0000000000..37a05f8d3e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt @@ -0,0 +1,59 @@ +package com.tangem.datasource.local.store + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update + +internal class RuntimeStore(private val keyProvider: (Data) -> Key) { + + private val store = MutableStateFlow>(hashMapOf()) + + fun get(selector: (Data) -> Boolean = { true }): Flow> { + return store.map { getInternal(it, selector) } + } + + fun getSync(selector: (Data) -> Boolean = { true }): List { + return getInternal(store.value, selector) + } + + // TODO: Uncomment if needed + // fun addOrReplace(items: Collection) { + // if (items.isEmpty()) return + // + // val storeValue = store.value + // + // items.forEach { item -> + // storeValue[keyProvider(item)] = item + // } + // + // store.value = storeValue + // } + + fun addOrReplace(item: Data) { + val storeValue = store.value + storeValue[keyProvider(item)] = item + + store.value = storeValue + } + + fun remove(selector: (Data) -> Boolean) { + val storeValue = store.value + + storeValue.forEach { (key, item) -> + if (selector(item)) { + storeValue.remove(key) + } + } + + store.value = storeValue + } + + fun clear() { + store.update { hashMapOf() } + } + + private fun getInternal(store: HashMap, selector: (Data) -> Boolean): List { + return store.values.filter { selector(it) } + } +} \ No newline at end of file diff --git a/data/common/.gitignore b/data/common/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts new file mode 100644 index 0000000000..7ddd807653 --- /dev/null +++ b/data/common/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +dependencies { + implementation(projects.core.datasource) + + implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/common/src/main/AndroidManifest.xml b/data/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5a762109a1 --- /dev/null +++ b/data/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt new file mode 100644 index 0000000000..fb698486ad --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt @@ -0,0 +1,52 @@ +package com.tangem.data.common.cache + +import org.joda.time.Duration + +/** + * Represents a registry for managing cache. + */ +interface CacheRegistry { + + /** + * Checks whether the cache key is expired. + * + * @param key cache key. + * @return `true` if the cache key is expired, `false` otherwise. + */ + suspend fun isExpired(key: String): Boolean + + /** + * Invalidates the cache key in registry. + * + * If the key doesn't exist, or it's already invalidated, this method doesn't have any effect. + * + * @param key cache key. + */ + suspend fun invalidate(key: String) + + /** + * Invalidates all cache keys in the registry. + * + * After the call, the registry doesn't contain any valid keys. + */ + suspend fun invalidateAll() + + /** + * Defines a callback to be invoked when the cache key expires. + * + * @param key cache key. + * @param skipCache if `true`, the callback will be invoked regardless of whether the key has expired or not. + * @param expireIn the duration after which the cache key is considered expired. + * @param block the block of code to be executed when the cache key expires. + */ + suspend fun invokeOnExpire( + key: String, + skipCache: Boolean, + expireIn: Duration = Duration.standardMinutes(DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES), + block: suspend () -> Unit, + ) + + private companion object { + const val DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES = 5L + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt new file mode 100644 index 0000000000..1aabdd2e77 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -0,0 +1,52 @@ +package com.tangem.data.common.cache + +import com.tangem.datasource.local.cache.CacheKeysStore +import com.tangem.datasource.local.cache.model.CacheKey +import org.joda.time.Duration +import org.joda.time.LocalDateTime + +internal class DefaultCacheRegistry( + private val cacheKeysStore: CacheKeysStore, +) : CacheRegistry { + + override suspend fun isExpired(key: String): Boolean { + val cacheKey = cacheKeysStore.get(key) ?: return true + + return cacheKey.updatedAt + .plus(cacheKey.expiresIn) + .isBefore(LocalDateTime.now()) + } + + override suspend fun invalidate(key: String) { + cacheKeysStore.remove(key) + } + + override suspend fun invalidateAll() { + cacheKeysStore.clear() + } + + override suspend fun invokeOnExpire( + key: String, + skipCache: Boolean, + expireIn: Duration, + block: suspend () -> Unit, + ) { + val isExpired = isExpired(key) || skipCache + if (!isExpired) return + + cacheKeysStore.addOrReplace( + key = CacheKey( + id = key, + updatedAt = LocalDateTime.now(), + expiresIn = expireIn, + ), + ) + + try { + block() + } catch (e: Throwable) { + invalidate(key) + throw e + } + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt new file mode 100644 index 0000000000..84359cb2e6 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt @@ -0,0 +1,21 @@ +package com.tangem.data.common.cache.di + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.cache.DefaultCacheRegistry +import com.tangem.datasource.local.cache.CacheKeysStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CacheRegistryModule { + + @Provides + @Singleton + fun provideCacheRegistry(cacheKeysStore: CacheKeysStore): CacheRegistry { + return DefaultCacheRegistry(cacheKeysStore) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cd9f9833e..276169d26e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,6 +85,7 @@ include(":domain:tokens") // endregion Domain modules // region Data modules +include(":data:common") include(":data:card") include(":data:tokens") include(":data:source:preferences") From 57e6f0ebb9080b928d6eb3c3d4534c62966aace6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 22 Jul 2023 19:34:34 +0400 Subject: [PATCH 059/100] Updated on 2026-08-14 --- .../tap/common/extensions/Blockchain.kt | 1 + .../tap/proxy/TransactionManagerImpl.kt | 4 +- .../res/drawable/ic_octaspace_no_color.xml | 58 +++++++++++++++++++ .../domain/common/extensions/Blockchain.kt | 5 ++ gradle/dependencies.toml | 2 +- 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_octaspace_no_color.xml diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 68aca58758..4d081cc2e6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -45,6 +45,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Cronos -> R.drawable.ic_cronos_no_color Blockchain.Telos, Blockchain.TelosTestnet -> R.drawable.ic_telos_no_color Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color + Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color else -> R.drawable.ic_tangem_logo } } diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 7c3e6f8b1e..57afd33a26 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -10,7 +10,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.blockchain.extensions.isNetworkError +import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -361,7 +361,7 @@ class TransactionManagerImpl( return SendTxResult.Success } is SimpleResult.Failure -> { - if (result.isNetworkError()) return SendTxResult.NetworkError(result.error) + if (ResultChecker.isNetworkError(result)) return SendTxResult.NetworkError(result.error) val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() when (error) { is BlockchainSdkError.WrappedTangemError -> { diff --git a/app/src/main/res/drawable/ic_octaspace_no_color.xml b/app/src/main/res/drawable/ic_octaspace_no_color.xml new file mode 100644 index 0000000000..36751027a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_octaspace_no_color.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 15fb408bde..8c306c3240 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -64,6 +64,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "telos/test" -> Blockchain.TelosTestnet "aleph-zero" -> Blockchain.AlephZero "aleph-zero/test" -> Blockchain.AlephZeroTestnet + "octaspace" -> Blockchain.OctaSpace + "octaspace/test" -> Blockchain.OctaSpaceTestnet else -> null } } @@ -132,6 +134,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.TelosTestnet -> "telos/test" Blockchain.AlephZero -> "aleph-zero" Blockchain.AlephZeroTestnet -> "aleph-zero/test" + Blockchain.OctaSpace -> "octaspace" + Blockchain.OctaSpaceTestnet -> "octaspace/test" } } @@ -175,6 +179,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Cronos -> "crypto-com-chain" Blockchain.Telos, Blockchain.TelosTestnet -> "telos" Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero" + Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f5f9705ca1..847e4191a7 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,7 +80,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-283" +tangemBlockchainSdk = "develop-289" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-273" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 025e41aa3863e3c1689f65c0ef8420ba23aa3e89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jul 2023 15:08:14 +0300 Subject: [PATCH 060/100] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 5ab4d720a3..5bac8dc2b2 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -7,6 +7,7 @@ import com.tangem.TangemSdk import com.tangem.blockchain.common.Blockchain import com.tangem.common.* import com.tangem.common.biometric.BiometricManager +import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.usersCode.UserCodeRepository From fcdfe622f9cc8e2aa2e0968e88ab9b0be7e28e7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jul 2023 22:35:58 +0800 Subject: [PATCH 061/100] Updated on 2026-08-14 --- app/build.gradle.kts | 3 + .../tangem/tap/di/domain/CardDomainModule.kt | 12 +- .../tap/di/domain/SettingsDomainModule.kt | 20 ++++ .../domain}/WalletsDomainModule.kt | 4 +- data/card/build.gradle.kts | 4 + .../tangem/data/card/DefaultCardRepository.kt | 16 +++ .../com/tangem/data/card/di/CardDataModule.kt | 12 ++ data/settings/.gitignore | 1 + data/settings/build.gradle.kts | 26 +++++ .../settings/DefaultSettingsRepository.kt | 18 +++ .../data/settings/di/SettingsDataModule.kt | 25 +++++ .../domain/card/GetCardWasScannedUseCase.kt | 8 ++ .../domain/card/repository/CardRepository.kt | 6 + .../tangem/domain/common/CardTypesResolver.kt | 16 +++ .../domain/common/TangemCardTypesResolver.kt | 22 ++++ domain/settings/.gitignore | 1 + domain/settings/build.gradle.kts | 4 + .../settings/IsUserAlreadyRateAppUseCase.kt | 8 ++ .../repositories/SettingsRepository.kt | 6 + features/wallet/impl/build.gradle.kts | 23 ++-- .../router/DefaultWalletRouter.kt | 37 +++---- .../wallet/state/WalletNotification.kt | 21 +++- .../state/builder/WalletStateFactory.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 23 +++- .../wallet/ui/components/WalletsList.kt | 8 -- .../WalletNotificationsListFactory.kt | 104 ++++++++++++++++++ settings.gradle.kts | 6 +- 27 files changed, 384 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt rename app/src/main/java/com/tangem/tap/{common/di/domain/wallets => di/domain}/WalletsDomainModule.kt (91%) create mode 100644 data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt create mode 100644 data/settings/.gitignore create mode 100644 data/settings/build.gradle.kts create mode 100644 data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt create mode 100644 data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt create mode 100644 domain/settings/.gitignore create mode 100644 domain/settings/build.gradle.kts create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b1db8e62c..0a8341a590 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,7 +28,9 @@ dependencies { implementation(projects.domain.card) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(project(":common")) implementation(project(":core:analytics")) implementation(projects.core.analytics.models) @@ -42,6 +44,7 @@ dependencies { implementation(project(":libs:auth")) implementation(project(":data:source:preferences")) implementation(projects.data.card) + implementation(projects.data.settings) implementation(projects.data.tokens) implementation(projects.data.common) diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 4af9f75d50..d2f09c474f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -1,9 +1,7 @@ package com.tangem.tap.di.domain -import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase -import com.tangem.domain.card.GetBiometricsStatusUseCase -import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.card.* +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import dagger.Module @@ -43,4 +41,10 @@ internal object CardDomainModule { ): GetAccessCodeSavingStatusUseCase { return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) } + + @Provides + @Singleton + fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase { + return GetCardWasScannedUseCase(cardRepository = cardRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt new file mode 100644 index 0000000000..4897db1d24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.settings.repositories.SettingsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object SettingsDomainModule { + + @Provides + @ViewModelScoped + fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase { + return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt similarity index 91% rename from app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt rename to app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index bcf08adf87..6fedc25c0e 100644 --- a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.di.domain.wallets +package com.tangem.tap.di.domain import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -11,7 +11,7 @@ import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) -object WalletsDomainModule { +internal object WalletsDomainModule { @Provides @ViewModelScoped diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 26b3010f2e..0999cc6959 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -13,10 +13,14 @@ android { dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) + implementation(deps.tangem.card.android) implementation(deps.tangem.card.core) + implementation(projects.core.utils) + implementation(projects.data.source.preferences) + implementation(projects.domain.card) implementation(projects.domain.models) } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt new file mode 100644 index 0000000000..b1bc033cb1 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.data.card + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.repository.CardRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultCardRepository( + private val preferencesDataSource: PreferencesDataSource, + private val dispatchers: CoroutineDispatcherProvider, +) : CardRepository { + + override suspend fun wasCardScanned(cardId: String): Boolean { + return withContext(dispatchers.io) { preferencesDataSource.usedCardsPrefStorage.wasScanned(cardId) } + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt index 11f63f4a31..b5edf95c45 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -1,9 +1,12 @@ package com.tangem.data.card.di +import com.tangem.data.card.DefaultCardRepository import com.tangem.data.card.DefaultCardSdkConfigRepository import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -25,4 +28,13 @@ internal object CardDataModule { preferencesDataSource = preferencesDataSource, ) } + + @Provides + @Singleton + fun provideCardRepository( + preferencesDataSource: PreferencesDataSource, + dispatchers: CoroutineDispatcherProvider, + ): CardRepository { + return DefaultCardRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers) + } } \ No newline at end of file diff --git a/data/settings/.gitignore b/data/settings/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/settings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts new file mode 100644 index 0000000000..1413053d65 --- /dev/null +++ b/data/settings/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.settings" +} + +dependencies { + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + implementation(deps.kotlin.coroutines) + + implementation(projects.core.utils) + + implementation(projects.domain.settings) + + implementation(projects.data.source.preferences) +} diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt new file mode 100644 index 0000000000..c1ee6fc57e --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.data.settings + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultSettingsRepository( + private val preferencesDataSource: PreferencesDataSource, + private val dispatchers: CoroutineDispatcherProvider, +) : SettingsRepository { + + override suspend fun isUserAlreadyRateApp(): Boolean { + return withContext(dispatchers.io) { + preferencesDataSource.appRatingLaunchObserver.isReadyToShow() + } + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt new file mode 100644 index 0000000000..6ef7e9a280 --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.settings.di + +import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SettingsDataModule { + + @Provides + @Singleton + fun provideSettingsRepository( + preferencesDataSource: PreferencesDataSource, + dispatchers: CoroutineDispatcherProvider, + ): SettingsRepository { + return DefaultSettingsRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt new file mode 100644 index 0000000000..91c077997d --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardRepository + +class GetCardWasScannedUseCase(private val cardRepository: CardRepository) { + + suspend operator fun invoke(cardId: String): Boolean = cardRepository.wasCardScanned(cardId) +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt new file mode 100644 index 0000000000..40ac6ec968 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.card.repository + +interface CardRepository { + + suspend fun wasCardScanned(cardId: String): Boolean +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index ed40b0b3b6..10d2f401bd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -26,4 +26,20 @@ interface CardTypesResolver { fun getPrimaryToken(): Token? fun getBackupCardsCount(): Int + + fun isReleaseFirmwareType(): Boolean + + fun getRemainingSignatures(): Int? + + fun getCardId(): String + + fun isTestCard(): Boolean + + fun isAttestationFailed(): Boolean + + fun hasWalletSignedHashes(): Boolean + + fun hasBackup(): Boolean + + fun isBackupForbidden(): Boolean } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 9f993a407b..0eb5a72085 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -10,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType +import com.tangem.operations.attestation.Attestation internal class TangemCardTypesResolver( private val card: CardDTO, @@ -73,6 +74,27 @@ internal class TangemCardTypesResolver( override fun getBackupCardsCount(): Int = card.wallets.size + override fun isReleaseFirmwareType(): Boolean = card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release + + override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures + + override fun getCardId(): String = card.cardId + + override fun isTestCard(): Boolean = card.isTestCard + + override fun isAttestationFailed(): Boolean = card.attestation.status == Attestation.Status.Failed + + override fun hasWalletSignedHashes(): Boolean { + return card.wallets.any { + val totalSignedHashes = it.totalSignedHashes ?: 0 + totalSignedHashes > 0 + } + } + + override fun hasBackup(): Boolean = card.backupStatus != CardDTO.BackupStatus.NoBackup + + override fun isBackupForbidden(): Boolean = !(card.settings.isBackupAllowed || card.settings.isHDWalletAllowed) + private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blockchain { // workaround for BSC (BNB) notes cards return when (blockchainName) { diff --git a/domain/settings/.gitignore b/domain/settings/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/settings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/settings/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt new file mode 100644 index 0000000000..fa71e80df8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class IsUserAlreadyRateAppUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isUserAlreadyRateApp() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt new file mode 100644 index 0000000000..c33c9d4cf7 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.settings.repositories + +interface SettingsRepository { + + suspend fun isUserAlreadyRateApp(): Boolean +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 55e9edc0c3..13a57f56c7 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -37,20 +37,21 @@ dependencies { kapt(deps.hilt.kapt) /** Core modules */ - implementation(project(":core:featuretoggles")) - implementation(project(":core:navigation")) - implementation(project(":core:ui")) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.ui) implementation(projects.core.utils) - /** Feature Apis */ - implementation(project(":features:wallet:api")) - /** Domain modules */ - implementation(project(":common")) + implementation(projects.common) implementation(projects.domain.card) - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:wallets")) - implementation(project(":domain:wallets:models")) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /** Feature Apis */ + implementation(projects.features.wallet.api) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2f30c3d355..3964582227 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -15,7 +15,6 @@ import androidx.navigation.compose.rememberNavController import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder -import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel @@ -35,29 +34,27 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation override fun Initialize(fragmentManager: FragmentManager) { this.fragmentManager = fragmentManager - TangemTheme { - NavHost( - navController = rememberNavController().apply { navController = this }, - startDestination = WalletScreens.WALLET.name, - ) { - composable(WalletScreens.WALLET.name) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) + NavHost( + navController = rememberNavController().apply { navController = this }, + startDestination = WalletScreens.WALLET.name, + ) { + composable(WalletScreens.WALLET.name) { + val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) - WalletScreen(state = viewModel.uiState) - } + WalletScreen(state = viewModel.uiState) + } - composable(WalletScreens.ORGANIZE_TOKENS.name) { - BackHandler(onBack = ::popBackStack) + composable(WalletScreens.ORGANIZE_TOKENS.name) { + BackHandler(onBack = ::popBackStack) - val viewModel: OrganizeTokensViewModel = hiltViewModel() - .apply { router = this@DefaultWalletRouter } + val viewModel: OrganizeTokensViewModel = hiltViewModel() + .apply { router = this@DefaultWalletRouter } - OrganizeTokensScreen( - modifier = Modifier.systemBarsPadding(), - state = viewModel.uiState, - ) - } + OrganizeTokensScreen( + modifier = Modifier.systemBarsPadding(), + state = viewModel.uiState, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt index 66079037c6..b1813c0544 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt @@ -63,12 +63,29 @@ sealed class WalletNotification(open val state: NotificationState) { ), ) + /** + * "Remaining signatures left" notification + * + * @param count number of remaining signatures + */ + class RemainingSignaturesLeft(count: Int) : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res( + id = R.string.warning_low_signatures_format, + formatArgs = WrappedList(data = listOf(count)), + ), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + /** * "Wallet already signed hashes" notification * * @property onClick lambda be invoked when notification is clicked */ - data class WalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + data class AlreadyToppedUpAndSignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( state = NotificationState.Clickable( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), @@ -83,7 +100,7 @@ sealed class WalletNotification(open val state: NotificationState) { * * @property onClick lambda be invoked when notification is clicked */ - data class MultiWalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + data class SignedTransactionsInThePast(override val onClick: () -> Unit) : Clickable, WalletNotification( state = NotificationState.Clickable( title = TextReference.Res( id = R.string.warning_important_security_info, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt index 21bf52bee0..081985c3f6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt @@ -37,7 +37,7 @@ internal class WalletStateFactory( walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), contentItems = persistentListOf(), - notifications = persistentListOf(), // TODO: create notifications + notifications = persistentListOf(), bottomSheet = WalletBottomSheetConfig( // TODO: check notifications isShow = false, @@ -58,7 +58,7 @@ internal class WalletStateFactory( walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), contentItems = persistentListOf(), - notifications = persistentListOf(), // TODO: create notifications + notifications = persistentListOf(), bottomSheet = WalletBottomSheetConfig( // TODO: check notifications isShow = false, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 5f825b106f..c777e06a49 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* @@ -40,6 +41,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator /** @@ -54,13 +56,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat @Composable internal fun WalletScreen(state: WalletStateHolder) { BackHandler(onBack = state.onBackClick) + val walletsListState = rememberLazyListState() Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - val walletsListState = rememberLazyListState() val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState) val pullRefreshState = rememberPullRefreshState( refreshing = state.pullToRefreshConfig.isRefreshing, @@ -153,8 +155,8 @@ internal fun WalletScreen(state: WalletStateHolder) { } } - PullRefreshIndicator( - refreshing = state.pullToRefreshConfig.isRefreshing, + PullToRefreshIndicator( + isRefreshing = state.pullToRefreshConfig.isRefreshing, state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter), ) @@ -164,6 +166,21 @@ internal fun WalletScreen(state: WalletStateHolder) { state.bottomSheet?.let { bottomSheetConfig -> if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig) } + + LaunchedEffect(key1 = walletsListState, key2 = state.walletsListConfig.onWalletChange) { + snapshotFlow { walletsListState.layoutInfo.visibleItemsInfo } + .collect(collector = ScrollOffsetCollector(callback = state.walletsListConfig.onWalletChange)) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) { + PullRefreshIndicator( + refreshing = isRefreshing, + state = state, + modifier = modifier, + ) } @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 3f162e41f0..34dc515850 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -11,8 +11,6 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview @@ -20,7 +18,6 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector /** * Wallets list component @@ -47,11 +44,6 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState WalletCard(state = state, modifier = Modifier.width(itemWidth)) } } - - LaunchedEffect(lazyListState) { - snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } - .collect(collector = ScrollOffsetCollector(callback = config.onWalletChange)) - } } @Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt new file mode 100644 index 0000000000..f2de50be61 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -0,0 +1,104 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") +internal class WalletNotificationsListFactory( + private val wasCardScannedCallback: suspend (String) -> Boolean, + private val isUserAlreadyRateAppCallback: suspend () -> Boolean, + private val onWalletAlreadySignedHashesClick: () -> Unit, + private val onMultiWalletAlreadySignedHashesClick: () -> Unit, + private val onBackupCardClick: () -> Unit, + private val hasUnreachableNetworksCallback: () -> Boolean, + private val onLikeTangemAppClick: () -> Unit, +) { + + fun create(cardTypesResolver: CardTypesResolver): Flow> { + // TODO: [REDACTED_JIRA] order + return flow { + emit( + buildList { + if (cardTypesResolver.isTestCard()) { + add(element = WalletNotification.TestCard) + return@buildList + } + + addRemainingSignaturesLeftNotifications(cardTypesResolver) + + if (!cardTypesResolver.isReleaseFirmwareType()) { + add(element = WalletNotification.DevCard) + } else { + addReleaseSpecialNotifications(cardTypesResolver) + } + + // TODO: [REDACTED_JIRA] - DemoModeDatasource + + if (hasUnreachableNetworksCallback()) { + add(element = WalletNotification.UnreachableNetworks) + } + + if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) { + add(element = WalletNotification.BackupCard(onClick = onBackupCardClick)) + } + + /* TODO: [REDACTED_JIRA] + * List + * .filter { store -> + * store.walletsData.any { it.status is WalletDataModel.MissedDerivation } + * } + */ + + if (isUserAlreadyRateAppCallback()) { + add(element = WalletNotification.LikeTangemApp(onClick = onLikeTangemAppClick)) + } + }.toImmutableList(), + ) + } + } + + private fun MutableList.addRemainingSignaturesLeftNotifications( + cardTypesResolver: CardTypesResolver, + ) { + val remainingSignatures = cardTypesResolver.getRemainingSignatures() + if (remainingSignatures != null && remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT) { + add(element = WalletNotification.RemainingSignaturesLeft(remainingSignatures)) + } + } + + private suspend fun MutableList.addReleaseSpecialNotifications( + cardTypesResolver: CardTypesResolver, + ) { + // TODO: [REDACTED_JIRA] !isDemo + if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed()) { + if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) { + add( + element = WalletNotification.SignedTransactionsInThePast( + onClick = onMultiWalletAlreadySignedHashesClick, + ), + ) + } else if (cardTypesResolver.hasWalletSignedHashes()) { + add( + element = WalletNotification.AlreadyToppedUpAndSignedHashes( + onClick = onWalletAlreadySignedHashesClick, + ), + ) + } + } + + if (cardTypesResolver.isAttestationFailed()) { + add(element = WalletNotification.CardVerificationFailed) + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 276169d26e..c77f58438b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -79,9 +79,10 @@ include(":domain:legacy") include(":domain:core") include(":domain:card") +include(":domain:settings") +include(":domain:tokens") include(":domain:wallets") include(":domain:wallets:models") -include(":domain:tokens") // endregion Domain modules // region Data modules @@ -89,4 +90,5 @@ include(":data:common") include(":data:card") include(":data:tokens") include(":data:source:preferences") -// endregion Data modules \ No newline at end of file +// endregion Data modules +include(":data:settings") \ No newline at end of file From effaf2d680114038e0f94082f15f5b508ea86428 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jul 2023 11:41:13 +0300 Subject: [PATCH 062/100] Updated on 2026-08-14 --- .../tap/domain/tokens/UserTokensRepository.kt | 6 +-- .../domain/tokens/UserTokensStorageService.kt | 1 + core/datasource/build.gradle.kts | 5 +- .../tangemTech/models/UserTokensResponse.kt | 26 +++++++++- .../com/tangem/datasource/di/MoshiModule.kt | 2 +- .../datasource/di/UserTokensStoreModule.kt | 22 ++++++++ .../local/cache/RuntimeCacheKeysStore.kt | 4 +- .../local/datastore/FileDataStore.kt | 52 +++++++++++++++++++ .../RuntimeDataStore.kt} | 4 +- .../local/token/FileUserTokensStore.kt | 28 ++++++++++ .../datasource/local/token/UserTokensStore.kt | 13 +++++ .../com/tangem/domain/core/error/DataError.kt | 7 +++ 12 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt rename core/datasource/src/main/java/com/tangem/datasource/local/{store/RuntimeStore.kt => datastore/RuntimeDataStore.kt} (91%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 6842f8a94c..c3ed94f70c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -79,8 +79,8 @@ class UserTokensRepository( private fun List.toUserTokensResponse(): UserTokensResponse { return UserTokensResponse( tokens = CurrencyConverter.convertList(input = this), - group = GROUP_DEFAULT_VALUE, - sort = SORT_DEFAULT_VALUE, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, ) } @@ -121,8 +121,6 @@ class UserTokensRepository( private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue companion object { - private const val GROUP_DEFAULT_VALUE = "none" - private const val SORT_DEFAULT_VALUE = "manual" private const val NOT_FOUND_HTTP_CODE = "404" // TODO("After adding DI") get dependencies by DI diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index b67bb497de..ff90f83942 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -7,6 +7,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.files.FileReader import com.tangem.tap.features.wallet.models.Currency +@Deprecated("Use [com.tangem.datasource.local.token.UserTokensStore] instead.") class UserTokensStorageService(private val fileReader: FileReader) { private val userTokensAdapter: JsonAdapter = MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java) diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index aa45701df5..34d4fa91a8 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -9,8 +9,9 @@ plugins { dependencies { /** Project */ - implementation(project(":core:utils")) - implementation(project(":libs:auth")) + implementation(projects.core.utils) + implementation(projects.libs.auth) + implementation(projects.domain.core) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 180deb01a0..c4b95bba88 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -4,8 +4,8 @@ import com.squareup.moshi.Json data class UserTokensResponse( @Json(name = "version") val version: Int = 0, - @Json(name = "group") val group: String? = null, - @Json(name = "sort") val sort: String? = null, + @Json(name = "group") val group: GroupType, + @Json(name = "sort") val sort: SortType, @Json(name = "tokens") val tokens: List = emptyList(), ) { @@ -18,4 +18,26 @@ data class UserTokensResponse( @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, ) + + enum class GroupType { + @Json(name = "none") + NONE, + + @Json(name = "token") + TOKEN, + + @Json(name = "network") + NETWORK, + } + + enum class SortType { + @Json(name = "balance") + BALANCE, + + @Json(name = "manual") + MANUAL, + + @Json(name = "marketcap") + MARKETCAP, + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 0ab6faea17..65668982d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -19,8 +19,8 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() - .add(KotlinJsonAdapterFactory()) .add(BigDecimalAdapter()) + .add(KotlinJsonAdapterFactory()) .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt new file mode 100644 index 0000000000..73d19f644d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.di + +import com.squareup.moshi.Moshi +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.token.FileUserTokensStore +import com.tangem.datasource.local.token.UserTokensStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UserTokensStoreModule { + + @Provides + @Singleton + fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore { + return FileUserTokensStore(fileReader, moshi) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt index 229b5ab30c..80bce89b0d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt @@ -1,11 +1,11 @@ package com.tangem.datasource.local.cache import com.tangem.datasource.local.cache.model.CacheKey -import com.tangem.datasource.local.store.RuntimeStore +import com.tangem.datasource.local.datastore.RuntimeDataStore internal class RuntimeCacheKeysStore : CacheKeysStore { - private val store = RuntimeStore(keyProvider = CacheKey::id) + private val store = RuntimeDataStore(keyProvider = CacheKey::id) override fun get(id: String): CacheKey? { return store.getSync { it.id == id }.firstOrNull() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt new file mode 100644 index 0000000000..2da02a5df8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -0,0 +1,52 @@ +package com.tangem.datasource.local.datastore + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.files.FileReader +import com.tangem.domain.core.error.DataError +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +internal class FileDataStore( + private val fileNameProvider: (key: String) -> String, + private val fileReader: FileReader, + private val adapter: JsonAdapter, +) { + + private val writeTrigger = MutableSharedFlow( + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + fun get(key: String): Flow { + return writeTrigger + .map { getInternal(fileNameProvider(key)) } + .filterNotNull() + } + + fun getSync(key: String): Data? { + return getInternal(fileNameProvider(key)) + } + + fun store(key: String, content: Data) { + try { + val json = adapter.toJson(content) + + fileReader.rewriteFile(json, fileNameProvider(key)) + writeTrigger.tryEmit(Unit) + } catch (e: Throwable) { + throw DataError.PersistenceError.UnableToWriteFile(e) + } + } + + private fun getInternal(fileName: String): Data? { + return try { + val json = fileReader.readFile(fileName) + + adapter.fromJson(json) + } catch (e: Throwable) { + throw DataError.PersistenceError.UnableToReadFile(e) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt similarity index 91% rename from core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index 37a05f8d3e..ba55ef6039 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -1,11 +1,11 @@ -package com.tangem.datasource.local.store +package com.tangem.datasource.local.datastore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -internal class RuntimeStore(private val keyProvider: (Data) -> Key) { +internal class RuntimeDataStore(private val keyProvider: (Data) -> Key) { private val store = MutableStateFlow>(hashMapOf()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt new file mode 100644 index 0000000000..1df1f44428 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.local.token + +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.datastore.FileDataStore +import kotlinx.coroutines.flow.Flow + +internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserTokensStore { + + private val store = FileDataStore( + fileNameProvider = { key -> "user_tokens_$key" }, + fileReader = fileReader, + adapter = moshi.adapter(UserTokensResponse::class.java), + ) + + override fun get(userWalletId: String): Flow { + return store.get(userWalletId) + } + + override suspend fun getSync(userWalletId: String): UserTokensResponse? { + return store.getSync(userWalletId) + } + + override suspend fun store(userWalletId: String, tokens: UserTokensResponse) { + store.store(userWalletId, tokens) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt new file mode 100644 index 0000000000..befab5e661 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import kotlinx.coroutines.flow.Flow + +interface UserTokensStore { + + fun get(userWalletId: String): Flow + + suspend fun getSync(userWalletId: String): UserTokensResponse? + + suspend fun store(userWalletId: String, tokens: UserTokensResponse) +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt index 11567ca43b..a76794c4c4 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -6,4 +6,11 @@ sealed class DataError : Exception() { object NoInternetConnection : NetworkError() } + + sealed class PersistenceError : DataError() { + + data class UnableToReadFile(override val cause: Throwable) : DataError() + + data class UnableToWriteFile(override val cause: Throwable) : DataError() + } } \ No newline at end of file From ddcb68caf6d59f7bb21a40491900a85daca489c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jul 2023 20:05:31 +0300 Subject: [PATCH 063/100] Updated on 2026-08-14 --- .../com/tangem/data/tokens/mock/MockTokens.kt | 50 +++++++++++-------- .../com/tangem/domain/tokens/model/Network.kt | 11 +++- .../com/tangem/domain/tokens/model/Token.kt | 19 ++++++- .../operations/TokenStatusOperations.kt | 2 +- .../tangem/domain/tokens/mock/MockTokens.kt | 32 +++++++----- .../domain/tokens/mock/MockTokensStates.kt | 20 ++++---- 6 files changed, 88 insertions(+), 46 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt index 4261727cff..3cf3584e68 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -5,7 +5,7 @@ import com.tangem.domain.wallets.models.UserWalletId internal object MockTokens { - val token1 get() = Token( + val token1 = Token( id = Token.ID("token1"), networkId = MockNetworks.network1.id, name = "Token 1", @@ -13,9 +13,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, + contractAddress = null, + derivationPath = null, ) - val token2 get() = Token( + val token2 = Token( id = Token.ID("token2"), networkId = MockNetworks.network1.id, name = "Token 2", @@ -23,9 +24,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token3 get() = Token( + val token3 = Token( id = Token.ID("token3"), networkId = MockNetworks.network1.id, name = "Token 3", @@ -33,9 +35,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token4 get() = Token( + val token4 = Token( id = Token.ID("token4"), networkId = MockNetworks.network2.id, name = "Token 4", @@ -43,9 +46,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, + contractAddress = null, + derivationPath = null, ) - val token5 get() = Token( + val token5 = Token( id = Token.ID("token5"), networkId = MockNetworks.network2.id, name = "Token 5", @@ -53,9 +57,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token6 get() = Token( + val token6 = Token( id = Token.ID("token6"), networkId = MockNetworks.network2.id, name = "Token 6", @@ -63,9 +68,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token7 get() = Token( + val token7 = Token( id = Token.ID("token7"), networkId = MockNetworks.network3.id, name = "Token 7", @@ -73,9 +79,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, + contractAddress = null, + derivationPath = null, ) - val token8 get() = Token( + val token8 = Token( id = Token.ID("token8"), networkId = MockNetworks.network3.id, name = "Token 8", @@ -83,9 +90,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token9 get() = Token( + val token9 = Token( id = Token.ID("token9"), networkId = MockNetworks.network3.id, name = "Token 9", @@ -93,9 +101,10 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) - val token10 get() = Token( + val token10 = Token( id = Token.ID("token10"), networkId = MockNetworks.network3.id, name = "Token 10", @@ -103,7 +112,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val tokens get() = mapOf( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt index b4b7628aa4..f7bd4500c2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt @@ -5,6 +5,15 @@ data class Network( val name: String, ) { + init { + require(name.isNotBlank()) { "Network name must not be blank" } + } + @JvmInline - value class ID(val value: String) + value class ID(val value: String) { + + init { + require(value.isNotBlank()) { "Network ID must not be blank" } + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt index 4e1a2fba61..745acaa6b5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt @@ -8,9 +8,24 @@ data class Token( val iconUrl: String?, val decimals: Int, val isCustom: Boolean, - val isCoin: Boolean, + val contractAddress: String?, + val derivationPath: String?, ) { + init { + require(name.isNotBlank()) { "Token name must not be blank" } + require(symbol.isNotBlank()) { "Token symbol must not be blank" } + require(iconUrl?.isNotBlank() ?: true) { "Token icon URL must not be blank" } + require(decimals > 0) { "Token decimal must not be less then zero, but it is: $decimals" } + require(contractAddress?.isNotBlank() ?: true) { "Token contract address must not be blank" } + require(derivationPath?.isNotBlank() ?: true) { "Token derivation path must not be blank" } + } + @JvmInline - value class ID(val value: String) + value class ID(val value: String) { + + init { + require(value.isNotBlank()) { "Token ID value must not be blank" } + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt index 04478c7917..dfb9d9f8bc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -21,7 +21,7 @@ internal class TokenStatusOperations( networkId = token.networkId, name = token.name, symbol = token.symbol, - isCoin = token.isCoin, + isCoin = token.contractAddress == null, decimals = token.decimals, iconUrl = token.iconUrl, value = createStatus(), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index 43f22db152..ec1d422785 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -13,8 +13,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, - + contractAddress = null, + derivationPath = null, ) val token2 = Token( id = Token.ID("token2"), @@ -24,7 +24,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token3 = Token( id = Token.ID("token3"), @@ -34,7 +35,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token4 = Token( id = Token.ID("token4"), @@ -44,8 +46,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, - + contractAddress = null, + derivationPath = null, ) val token5 = Token( id = Token.ID("token5"), @@ -55,7 +57,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token6 = Token( id = Token.ID("token6"), @@ -65,7 +68,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token7 = Token( id = Token.ID("token7"), @@ -75,7 +79,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = true, + contractAddress = null, + derivationPath = null, ) val token8 = Token( id = Token.ID("token8"), @@ -85,7 +90,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token9 = Token( id = Token.ID("token9"), @@ -95,7 +101,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val token10 = Token( id = Token.ID("token10"), @@ -105,7 +112,8 @@ internal object MockTokens { isCustom = false, decimals = 8, iconUrl = null, - isCoin = false, + contractAddress = "address", + derivationPath = null, ) val tokens = nonEmptySetOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 0b0c9e41ab..9b5677d97e 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -14,7 +14,7 @@ internal object MockTokensStates { symbol = MockTokens.token1.symbol, decimals = MockTokens.token1.decimals, iconUrl = MockTokens.token1.iconUrl, - isCoin = MockTokens.token1.isCoin, + isCoin = MockTokens.token1.contractAddress == null, value = TokenStatus.Unreachable, ) @@ -25,7 +25,7 @@ internal object MockTokensStates { symbol = MockTokens.token2.symbol, decimals = MockTokens.token2.decimals, iconUrl = MockTokens.token2.iconUrl, - isCoin = MockTokens.token2.isCoin, + isCoin = MockTokens.token2.contractAddress == null, value = TokenStatus.Unreachable, ) @@ -36,7 +36,7 @@ internal object MockTokensStates { symbol = MockTokens.token3.symbol, decimals = MockTokens.token3.decimals, iconUrl = MockTokens.token3.iconUrl, - isCoin = MockTokens.token3.isCoin, + isCoin = MockTokens.token3.contractAddress == null, value = TokenStatus.Unreachable, ) @@ -47,7 +47,7 @@ internal object MockTokensStates { symbol = MockTokens.token4.symbol, decimals = MockTokens.token4.decimals, iconUrl = MockTokens.token4.iconUrl, - isCoin = MockTokens.token4.isCoin, + isCoin = MockTokens.token4.contractAddress == null, value = TokenStatus.MissedDerivation, ) @@ -58,7 +58,7 @@ internal object MockTokensStates { symbol = MockTokens.token5.symbol, decimals = MockTokens.token5.decimals, iconUrl = MockTokens.token5.iconUrl, - isCoin = MockTokens.token5.isCoin, + isCoin = MockTokens.token5.contractAddress == null, value = TokenStatus.MissedDerivation, ) @@ -69,7 +69,7 @@ internal object MockTokensStates { symbol = MockTokens.token6.symbol, decimals = MockTokens.token6.decimals, iconUrl = MockTokens.token6.iconUrl, - isCoin = MockTokens.token6.isCoin, + isCoin = MockTokens.token6.contractAddress == null, value = TokenStatus.MissedDerivation, ) @@ -80,7 +80,7 @@ internal object MockTokensStates { symbol = MockTokens.token7.symbol, decimals = MockTokens.token7.decimals, iconUrl = MockTokens.token7.iconUrl, - isCoin = MockTokens.token7.isCoin, + isCoin = MockTokens.token7.contractAddress == null, value = TokenStatus.NoAccount, ) @@ -91,7 +91,7 @@ internal object MockTokensStates { symbol = MockTokens.token8.symbol, decimals = MockTokens.token8.decimals, iconUrl = MockTokens.token8.iconUrl, - isCoin = MockTokens.token8.isCoin, + isCoin = MockTokens.token8.contractAddress == null, value = TokenStatus.NoAccount, ) @@ -102,7 +102,7 @@ internal object MockTokensStates { symbol = MockTokens.token9.symbol, decimals = MockTokens.token9.decimals, iconUrl = MockTokens.token9.iconUrl, - isCoin = MockTokens.token9.isCoin, + isCoin = MockTokens.token9.contractAddress == null, value = TokenStatus.NoAccount, ) @@ -113,7 +113,7 @@ internal object MockTokensStates { symbol = MockTokens.token10.symbol, decimals = MockTokens.token10.decimals, iconUrl = MockTokens.token10.iconUrl, - isCoin = MockTokens.token10.isCoin, + isCoin = MockTokens.token10.contractAddress == null, value = TokenStatus.NoAccount, ) From 782a2ec0f5f8819cb0c51dec8c3de84c48a56380 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 10:52:33 +0500 Subject: [PATCH 064/100] Updated on 2026-08-14 --- .../tangem/tap/proxy/TxHistoryManagerImpl.kt | 86 +++++++++++++++++++ .../com/tangem/tap/proxy/di/ProxyModule.kt | 12 ++- data/txhistory/.gitignore | 1 + data/txhistory/build.gradle.kts | 21 +++++ .../data/txhistory/di/TxHistoryDataModule.kt | 18 ++++ .../data/txhistory/mock/MockTxHistoryItems.kt | 70 +++++++++++++++ .../repository/MockTxHistoryRepository.kt | 25 ++++++ .../paging/TxHistoryPagingSource.kt | 41 +++++++++ domain/txhistory/.gitignore | 1 + domain/txhistory/build.gradle.kts | 17 ++++ .../txhistory/error/TxHistoryListError.kt | 5 ++ .../txhistory/error/TxHistoryStateError.kt | 10 +++ .../domain/txhistory/model/TxHistoryItem.kt | 23 +++++ .../repository/TxHistoryRepository.kt | 16 ++++ .../usecase/GetTxHistoryItemsCountUseCase.kt | 27 ++++++ .../usecase/GetTxHistoryItemsUseCase.kt | 49 +++++++++++ .../com/tangem/lib/crypto/TxHistoryManager.kt | 18 ++++ .../txhistory/ProxyTransactionHistoryItem.kt | 21 +++++ .../txhistory/ProxyTransactionHistoryState.kt | 15 ++++ .../txhistory/ProxyTransactionStatus.kt | 3 + settings.gradle.kts | 6 +- 21 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt create mode 100644 data/txhistory/.gitignore create mode 100644 data/txhistory/build.gradle.kts create mode 100644 data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt create mode 100644 data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt create mode 100644 data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt create mode 100644 data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt create mode 100644 domain/txhistory/.gitignore create mode 100644 domain/txhistory/build.gradle.kts create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt diff --git a/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt new file mode 100644 index 0000000000..ff8b430a00 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt @@ -0,0 +1,86 @@ +package com.tangem.tap.proxy + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.txhistory.TransactionHistoryItem +import com.tangem.blockchain.common.txhistory.TransactionHistoryState +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.lib.crypto.TxHistoryManager +import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionStatus + +class TxHistoryManagerImpl( + private val appStateHolder: AppStateHolder, +) : TxHistoryManager { + + override suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val state = walletManager.getTransactionHistoryState(address = walletManager.wallet.address) + return state.mapToProxy() + } + + override suspend fun getTxHistoryItems( + networkId: String, + derivationPath: String?, + page: Int, + pageSize: Int, + ): List { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val itemsResult = walletManager.getTransactionsHistory( + address = walletManager.wallet.address, + page = page, + pageSize = pageSize, + ) + + return when (itemsResult) { + is Result.Success -> itemsResult.data.map { historyItem -> historyItem.mapToProxy() } + is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) + } + } + + private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) + val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + return requireNotNull(walletManager) { "no wallet manager found" } + } + + private fun TransactionHistoryState.mapToProxy(): ProxyTransactionHistoryState { + return when (this) { + TransactionHistoryState.Success.Empty -> ProxyTransactionHistoryState.Success.Empty + is TransactionHistoryState.Failed.FetchError -> ProxyTransactionHistoryState.Failed.FetchError(exception) + TransactionHistoryState.NotImplemented -> ProxyTransactionHistoryState.NotImplemented + is TransactionHistoryState.Success.HasTransactions -> + ProxyTransactionHistoryState.Success.HasTransactions(txCount) + } + } + + private fun TransactionHistoryItem.mapToProxy() = ProxyTransactionHistoryItem( + txHash = txHash, + timestamp = timestamp, + direction = when (val direction = direction) { + is TransactionHistoryItem.TransactionDirection.Incoming -> + ProxyTransactionHistoryItem.TransactionDirection.Incoming(direction.from) + is TransactionHistoryItem.TransactionDirection.Outgoing -> + ProxyTransactionHistoryItem.TransactionDirection.Outgoing(direction.to) + }, + status = when (status) { + TransactionStatus.Confirmed -> ProxyTransactionStatus.Confirmed + TransactionStatus.Unconfirmed -> ProxyTransactionStatus.Unconfirmed + }, + type = when (type) { + TransactionHistoryItem.TransactionType.Transfer -> ProxyTransactionHistoryItem.TransactionType.Transfer + }, + amount = ProxyAmount( + currencySymbol = amount.currencySymbol, + value = requireNotNull(amount.value) { "Amount value must not be null" }, + decimals = amount.decimals, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index cf7f41785a..9bae439352 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -8,11 +8,9 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager +import com.tangem.lib.crypto.TxHistoryManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.proxy.DerivationManagerImpl -import com.tangem.tap.proxy.TransactionManagerImpl -import com.tangem.tap.proxy.UserWalletManagerImpl +import com.tangem.tap.proxy.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -61,6 +59,12 @@ class ProxyModule { ) } + @Provides + @Singleton + fun provideTxHistoryManager(appStateHolder: AppStateHolder): TxHistoryManager { + return TxHistoryManagerImpl(appStateHolder = appStateHolder) + } + // regions FeatureConsumers @Provides @Singleton diff --git a/data/txhistory/.gitignore b/data/txhistory/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/txhistory/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts new file mode 100644 index 0000000000..d2aa98d0fd --- /dev/null +++ b/data/txhistory/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.txhistory" +} + +dependencies { + implementation(projects.domain.txhistory) + + implementation(deps.kotlin.coroutines) + implementation(deps.androidx.paging.runtime) + implementation(deps.arrow.core) + + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt new file mode 100644 index 0000000000..6fe0d348de --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.txhistory.di + +import com.tangem.data.txhistory.repository.MockTxHistoryRepository +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TxHistoryDataModule { + + @Provides + @Singleton + fun provideTxHistoryRepository(): TxHistoryRepository = MockTxHistoryRepository() +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt new file mode 100644 index 0000000000..4f7cbec344 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt @@ -0,0 +1,70 @@ +package com.tangem.data.txhistory.mock + +import com.tangem.domain.txhistory.model.TxHistoryItem +import java.math.BigDecimal + +internal object MockTxHistoryItems { + + private val txHistoryItem1 = TxHistoryItem( + txHash = "noster", + timestamp = 1689930746, + direction = TxHistoryItem.TransactionDirection.Incoming("address"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem2 = TxHistoryItem( + txHash = "noster", + timestamp = 1689844346, + direction = TxHistoryItem.TransactionDirection.Incoming("address2"), + status = TxHistoryItem.TxStatus.Unconfirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem3 = TxHistoryItem( + txHash = "noster", + timestamp = 1689757946, + direction = TxHistoryItem.TransactionDirection.Outgoing("address3"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem4 = TxHistoryItem( + txHash = "noster", + timestamp = 1689671546, + direction = TxHistoryItem.TransactionDirection.Incoming("address4"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem5 = TxHistoryItem( + txHash = "noster", + timestamp = 1689585146, + direction = TxHistoryItem.TransactionDirection.Outgoing("address5"), + status = TxHistoryItem.TxStatus.Unconfirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem6 = TxHistoryItem( + txHash = "noster", + timestamp = 1689585146, + direction = TxHistoryItem.TransactionDirection.Incoming("address6"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + val txHistoryItems = listOf( + txHistoryItem1, + txHistoryItem2, + txHistoryItem3, + txHistoryItem4, + txHistoryItem5, + txHistoryItem6, + ) +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt new file mode 100644 index 0000000000..59829686e4 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt @@ -0,0 +1,25 @@ +package com.tangem.data.txhistory.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import kotlinx.coroutines.flow.Flow + +internal class MockTxHistoryRepository : TxHistoryRepository { + + override suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int { + return 0 + } + + override fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> { + return Pager( + config = PagingConfig( + pageSize = pageSize, + ), + pagingSourceFactory = { TxHistoryPagingSource() }, + ).flow + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt new file mode 100644 index 0000000000..3eba3ed6ce --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -0,0 +1,41 @@ +package com.tangem.data.txhistory.repository.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.tangem.data.txhistory.mock.MockTxHistoryItems +import com.tangem.domain.txhistory.model.TxHistoryItem + +private const val INITIAL_PAGE = 1 + +internal class TxHistoryPagingSource : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val currentPage = params.key ?: INITIAL_PAGE + return try { + // TODO: [REDACTED_JIRA] + // val result = txHistoryManager.getTxHistoryItems( + // networkId = networkId, + // derivationPath = derivationPath, + // page = currentPage, + // pageSize = params.loadSize, + // ) + val result = MockTxHistoryItems.txHistoryItems + + LoadResult.Page( + data = result, + prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null, + // TODO: handle end of reached [REDACTED_JIRA] + nextKey = null, + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/domain/txhistory/.gitignore b/domain/txhistory/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/txhistory/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts new file mode 100644 index 0000000000..1237e8f782 --- /dev/null +++ b/domain/txhistory/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.txhistory" +} + +dependencies { + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.androidx.paging.runtime) + + implementation(projects.core.utils) +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt new file mode 100644 index 0000000000..1538b27e0b --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.txhistory.error + +sealed class TxHistoryListError : Throwable() { + data class DataError(override val cause: Throwable) : TxHistoryListError() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt new file mode 100644 index 0000000000..3f93a3cf12 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.txhistory.error + +sealed class TxHistoryStateError : Throwable() { + + object TxHistoryNotImplemented : TxHistoryStateError() + + object EmptyTxHistories : TxHistoryStateError() + + data class DataError(override val cause: Throwable) : TxHistoryStateError() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt new file mode 100644 index 0000000000..41d5c76d8e --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.txhistory.model + +import java.math.BigDecimal + +data class TxHistoryItem( + val txHash: String, + val timestamp: Long, + val direction: TransactionDirection, + val status: TxStatus, + val type: TransactionType, + val amount: BigDecimal, +) { + sealed interface TransactionDirection { + data class Incoming(val from: String) : TransactionDirection + data class Outgoing(val to: String) : TransactionDirection + } + + sealed interface TransactionType { + object Transfer : TransactionType + } + + enum class TxStatus { Confirmed, Unconfirmed } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt new file mode 100644 index 0000000000..1ebb8ae1dd --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.txhistory.repository + +import androidx.paging.PagingData +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.model.TxHistoryItem +import kotlinx.coroutines.flow.Flow + +interface TxHistoryRepository { + + @Throws(TxHistoryStateError::class) + suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int + + @Throws(TxHistoryListError::class) + fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt new file mode 100644 index 0000000000..c6c0b347b3 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.txhistory.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.repository.TxHistoryRepository + +class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { + + suspend operator fun invoke(networkId: String, derivationPath: String): Either { + return either { + catch( + block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, + catch = { e -> + raise( + when (e) { + is TxHistoryStateError.TxHistoryNotImplemented -> e + is TxHistoryStateError.EmptyTxHistories -> e + else -> TxHistoryStateError.DataError(e) + }, + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt new file mode 100644 index 0000000000..48d2759704 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.txhistory.usecase + +import androidx.paging.PagingData +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.recover +import arrow.core.right +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collect + +private const val DEFAULT_PAGE_SIZE = 20 + +class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { + + operator fun invoke( + networkId: String, + pageSize: Int = DEFAULT_PAGE_SIZE, + ): Flow,>,> { + return channelFlow { + recover( + block = { + getTxHistoryItems( + networkId = networkId, + pageSize = pageSize, + ).collect { items -> + send(items.right()) + } + }, + recover = { error -> send(error.left()) }, + ) + } + } + + private fun Raise.getTxHistoryItems( + networkId: String, + pageSize: Int, + ): Flow> { + return repository + .getTxHistoryItems(networkId = networkId, pageSize = pageSize) + .catch { raise(TxHistoryListError.DataError(it)) } + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt new file mode 100644 index 0000000000..de50d8b4ee --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt @@ -0,0 +1,18 @@ +package com.tangem.lib.crypto + +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState + +interface TxHistoryManager { + + @Throws(IllegalStateException::class) + suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState + + @Throws(IllegalStateException::class) + suspend fun getTxHistoryItems( + networkId: String, + derivationPath: String?, + page: Int, + pageSize: Int, + ): List +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt new file mode 100644 index 0000000000..387a7518e6 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt @@ -0,0 +1,21 @@ +package com.tangem.lib.crypto.models.txhistory + +import com.tangem.lib.crypto.models.ProxyAmount + +data class ProxyTransactionHistoryItem( + val txHash: String, + val timestamp: Long, + val direction: TransactionDirection, + val status: ProxyTransactionStatus, + val type: TransactionType, + val amount: ProxyAmount, +) { + sealed interface TransactionDirection { + data class Incoming(val from: String) : TransactionDirection + data class Outgoing(val to: String) : TransactionDirection + } + + sealed interface TransactionType { + object Transfer : TransactionType + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt new file mode 100644 index 0000000000..ddeeba4d3f --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt @@ -0,0 +1,15 @@ +package com.tangem.lib.crypto.models.txhistory + +sealed class ProxyTransactionHistoryState { + + sealed class Success : ProxyTransactionHistoryState() { + object Empty : Success() + data class HasTransactions(val txCount: Int) : Success() + } + + sealed class Failed : ProxyTransactionHistoryState() { + data class FetchError(val exception: Exception) : Failed() + } + + object NotImplemented : ProxyTransactionHistoryState() +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt new file mode 100644 index 0000000000..2e5ffd5aaf --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt @@ -0,0 +1,3 @@ +package com.tangem.lib.crypto.models.txhistory + +enum class ProxyTransactionStatus { Confirmed, Unconfirmed } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c77f58438b..675e2f1073 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -83,6 +83,7 @@ include(":domain:settings") include(":domain:tokens") include(":domain:wallets") include(":domain:wallets:models") +include(":domain:txhistory") // endregion Domain modules // region Data modules @@ -90,5 +91,6 @@ include(":data:common") include(":data:card") include(":data:tokens") include(":data:source:preferences") -// endregion Data modules -include(":data:settings") \ No newline at end of file +include(":data:settings") +include(":data:txhistory") +// endregion Data modules \ No newline at end of file From 3e71ba89acc8f5ffa819f78ac50e558d321570dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jul 2023 19:42:06 +0300 Subject: [PATCH 065/100] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 21 +++++++++++++++++++ .../tap/di/data/UserWalletsStoreModule.kt | 21 +++++++++++++++++++ core/datasource/build.gradle.kts | 1 + .../local/userwallet/UserWalletsStore.kt | 9 ++++++++ 4 files changed, 52 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt create mode 100644 app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt new file mode 100644 index 0000000000..7542283107 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.data + +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.firstOrNull + +// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented +// [REDACTED_JIRA] +internal class RuntimeUserWalletsStore( + private val walletsStateHolder: WalletsStateHolder, +) : UserWalletsStore { + + override suspend fun getSync(userWalletId: UserWalletId): UserWallet? { + return walletsStateHolder.userWalletsListManager + ?.userWallets + ?.firstOrNull() + ?.firstOrNull { it.walletId == userWalletId } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt new file mode 100644 index 0000000000..6aeaad1dcf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.di.data + +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.tap.data.RuntimeUserWalletsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UserWalletsStoreModule { + + @Provides + @Singleton + fun provideUserWalletsStore(walletsStateHolder: WalletsStateHolder): UserWalletsStore { + return RuntimeUserWalletsStore(walletsStateHolder) + } +} \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 34d4fa91a8..7f08451e5b 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.libs.auth) implementation(projects.domain.core) + implementation(projects.domain.wallets.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt new file mode 100644 index 0000000000..9b4e2693b7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.local.userwallet + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +interface UserWalletsStore { + + suspend fun getSync(userWalletId: UserWalletId): UserWallet? +} \ No newline at end of file From bfdac6a9a87db0504eb8457851e0e8500eeccd29 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Jul 2023 10:25:39 +0300 Subject: [PATCH 066/100] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 9 +++ .../tokens/ToggleTokenListGroupingUseCase.kt | 81 +++++++++++++++++++ .../tokens/error/TokenListSortingError.kt | 2 + 3 files changed, 92 insertions(+) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index a43ebfeb6c..c5f6850336 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -26,6 +26,15 @@ internal object TokensDomainModule { return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideToggleTokenListGroupingUseCase( + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): ToggleTokenListGroupingUseCase { + return ToggleTokenListGroupingUseCase(networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt new file mode 100644 index 0000000000..4d65f82422 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -0,0 +1,81 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.TokenListSortingOperations +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class ToggleTokenListGroupingUseCase( + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke(tokenList: TokenList): Either { + return withContext(dispatchers.default) { + either { + ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + TokenListSortingError.TokenListIsLoading + } + + when (tokenList) { + is TokenList.GroupedByNetwork -> ungroupTokens(tokenList) + is TokenList.Ungrouped -> groupTokens(tokenList) + is TokenList.NotInitialized -> raise(TokenListSortingError.TokenListIsEmpty) + } + } + } + } + + private suspend fun Raise.groupTokens( + tokenList: TokenList.Ungrouped, + ): TokenList.GroupedByNetwork { + val sortingOperations = getSortingOperations(tokenList) + val tokens = sortingOperations.getTokens() + + val networks = getNetworks(tokens.map { it.networkId }.toSet()) + return TokenList.GroupedByNetwork( + groups = sortingOperations.getGroupedTokens(networks), + totalFiatBalance = tokenList.totalFiatBalance, + sortedBy = sortingOperations.getSortType(), + ) + } + + private suspend fun Raise.ungroupTokens( + tokenList: TokenList.GroupedByNetwork, + ): TokenList.Ungrouped { + val sortingOperations = getSortingOperations(tokenList) + + return TokenList.Ungrouped( + tokens = sortingOperations.getTokens(), + totalFiatBalance = tokenList.totalFiatBalance, + sortedBy = sortingOperations.getSortType(), + ) + } + + private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { + return TokenListSortingOperations( + tokenList = tokenList, + dispatchers = dispatchers, + raise = this, + transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, + ) + } + + private suspend fun Raise.getNetworks(networksIds: Set): Set { + return withContext(dispatchers.io) { + catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt index ae49dfb209..847432134b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt @@ -7,4 +7,6 @@ sealed class TokenListSortingError { object TokenListIsEmpty : TokenListSortingError() object UnableToSortTokenList : TokenListSortingError() + + data class DataError(val cause: Throwable) : TokenListSortingError() } \ No newline at end of file From 47fe92dc94db88372a1f120c696ae79bf056c45d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 15 Jul 2023 15:18:28 +0300 Subject: [PATCH 067/100] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 9 ++++ .../tokens/repository/MockTokensRepository.kt | 2 +- .../tokens/ApplyTokenListSortingUseCase.kt | 49 +++++++++++++++++++ .../tokens/repository/NetworksRepository.kt | 3 -- .../tokens/repository/QuotesRepository.kt | 3 -- .../tokens/repository/TokensRepository.kt | 5 +- .../tokens/repository/MockTokensRepository.kt | 2 +- 7 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c5f6850336..b593c7a762 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -40,4 +40,13 @@ internal object TokensDomainModule { fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { return ToggleTokenListSortingUseCase(dispatchers) } + + @Provides + @ViewModelScoped + fun provideApplyTokenListSortingUseCase( + tokensRepository: TokensRepository, + dispatchers: CoroutineDispatcherProvider, + ): ApplyTokenListSortingUseCase { + return ApplyTokenListSortingUseCase(tokensRepository, dispatchers) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt index e6b7760a7c..8c9ec119a7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt @@ -11,7 +11,7 @@ internal class MockTokensRepository : TokensRepository { override suspend fun sortTokens( userWalletId: UserWalletId, - sortedTokensIds: Set, + sortedTokens: Set, isGrouped: Boolean, isSortedByBalance: Boolean, ) = Unit diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt new file mode 100644 index 0000000000..2f51531dc2 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class ApplyTokenListSortingUseCase( + private val tokensRepository: TokensRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + sortedTokens: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): Either { + return withContext(dispatchers.default) { + either { + val nonEmptyTokens = ensureNotNull(sortedTokens.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + + applySorting(userWalletId, nonEmptyTokens, isGrouped, isSortedByBalance) + } + } + } + + private suspend fun Raise.applySorting( + userWalletId: UserWalletId, + tokens: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ) = withContext(dispatchers.io) { + catch( + block = { tokensRepository.sortTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 93f7f7c95b..97efc6c34a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -6,9 +6,6 @@ import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow -// FIXME: Use Raise as context instead of Effect when context receivers become stable -// [REDACTED_JIRA] - /** * Repository for everything related to the blockchain networks * */ diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 984ffb6608..fd3e10aaff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -4,9 +4,6 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Token import kotlinx.coroutines.flow.Flow -// FIXME: Use Raise as context instead of Effect when context receivers become stable -// [REDACTED_JIRA] - /** * Repository for everything related to the quotes of tokens * */ diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index b544ecb8f5..1f978018a3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -4,9 +4,6 @@ import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow -// FIXME: Use Raise as context instead of Effect when context receivers become stable -// [REDACTED_JIRA] - /** * Repository for everything related to the tokens of user wallet * */ @@ -14,7 +11,7 @@ interface TokensRepository { suspend fun sortTokens( userWalletId: UserWalletId, - sortedTokensIds: Set, + sortedTokens: Set, isGrouped: Boolean, isSortedByBalance: Boolean, ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index 813816c52a..c5db701f0b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -16,7 +16,7 @@ internal class MockTokensRepository( override suspend fun sortTokens( userWalletId: UserWalletId, - sortedTokensIds: Set, + sortedTokens: Set, isGrouped: Boolean, isSortedByBalance: Boolean, ) = Unit From 159468fc396b0bc04ef59f358a19c450936c6f20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 15 Jul 2023 15:20:48 +0300 Subject: [PATCH 068/100] Updated on 2026-08-14 --- .../tokens/repository/MockTokensRepository.kt | 6 +- .../tokens/ApplyTokenListSortingUseCase.kt | 2 +- .../tokens/repository/TokensRepository.kt | 6 +- .../ApplyTokenListSortingUseCaseTest.kt | 166 ++++++++++++++++ .../domain/tokens/GetTokenListUseCaseTest.kt | 10 +- .../tokens/ToggleTokenListGroupingTest.kt | 180 ++++++++++++++++++ .../ToggleTokenListSortingUseCaseTest.kt | 143 ++++++++++++++ .../domain/tokens/mock/MockNetworksGroups.kt | 52 +++++ .../domain/tokens/mock/MockTokenLists.kt | 119 ++++++------ .../tangem/domain/tokens/mock/MockTokens.kt | 3 +- .../domain/tokens/mock/MockTokensStates.kt | 6 +- .../tokens/repository/MockTokensRepository.kt | 24 ++- 12 files changed, 642 insertions(+), 75 deletions(-) create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt index 8c9ec119a7..59e58db799 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.flowOf internal class MockTokensRepository : TokensRepository { - override suspend fun sortTokens( + override suspend fun saveTokens( userWalletId: UserWalletId, - sortedTokens: Set, - isGrouped: Boolean, + tokens: Set, + isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) = Unit diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 2f51531dc2..53a38d6642 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -42,7 +42,7 @@ class ApplyTokenListSortingUseCase( isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { catch( - block = { tokensRepository.sortTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, + block = { tokensRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, catch = { raise(TokenListSortingError.DataError(it)) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index 1f978018a3..06e0b286b3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.Flow * */ interface TokensRepository { - suspend fun sortTokens( + suspend fun saveTokens( userWalletId: UserWalletId, - sortedTokens: Set, - isGrouped: Boolean, + tokens: Set, + isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt new file mode 100644 index 0000000000..0af91c7c22 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -0,0 +1,166 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.repository.MockTokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class ApplyTokenListSortingUseCaseTest { + + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when tokens are empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokens = emptySet(), + isGrouped = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens saving failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val repository = getTokensRepository( + sortTokensResult = DataError.NetworkError.NoInternetConnection.left(), + ) + val useCase = getUseCase(repository) + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokens = MockTokens.tokens, + isGrouped = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = MockTokens.tokens + val expectedIsGrouped = true + val expectedIsSorted = true + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokens = expectedTokens, + isGrouped = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = MockTokens.tokens + val expectedIsGrouped = true + val expectedIsSorted = false + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokens = expectedTokens, + isGrouped = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = MockTokens.tokens + val expectedIsGrouped = false + val expectedIsSorted = true + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokens = expectedTokens, + isGrouped = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = MockTokens.tokens + val expectedIsGrouped = false + val expectedIsSorted = false + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokens = expectedTokens, + isGrouped = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + private fun getUseCase(tokensRepository: MockTokensRepository = getTokensRepository()) = + ApplyTokenListSortingUseCase( + tokensRepository = tokensRepository, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private fun getTokensRepository(sortTokensResult: Either = Unit.right()) = + MockTokensRepository(sortTokensResult, emptyFlow(), emptyFlow(), emptyFlow()) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index fb26a27895..8cb8237f21 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -31,7 +31,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest { // Given - val expectedResult = MockTokenLists.ungroupedTokenList.right() + val expectedResult = MockTokenLists.failedUngroupedTokenList.right() val useCase = getUseCase( isGrouped = flowOf(false.right()), @@ -137,7 +137,7 @@ internal class GetTokenListUseCaseTest { // Given val error = DataError.NetworkError.NoInternetConnection.left() val expectedResult = listOf( - MockTokenLists.ungroupedTokenList.right(), + MockTokenLists.failedUngroupedTokenList.right(), TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(), ) @@ -159,7 +159,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when list grouped then correct token list should be received`() = runTest { - val expectedResult = MockTokenLists.groupedTokenList.right() + val expectedResult = MockTokenLists.failedGroupedTokenList.right() val useCase = getUseCase(isGrouped = flowOf(true.right())) @@ -235,7 +235,7 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { - val expectedResult = TokenListError.UnableToSortTokenList(MockTokenLists.ungroupedTokenList).left() + val expectedResult = TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left() val useCase = getUseCase( networks = emptySet().right(), @@ -326,7 +326,7 @@ internal class GetTokenListUseCaseTest { isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), ) = GetTokenListUseCase( dispatchers = dispatchers, - tokensRepository = MockTokensRepository(tokens, isGrouped, isSortedByBalance), + tokensRepository = MockTokensRepository(Unit.right(), tokens, isGrouped, isSortedByBalance), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(networks, statuses), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt new file mode 100644 index 0000000000..0faaff77ea --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt @@ -0,0 +1,180 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.MockNetworksRepository +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class ToggleTokenListGroupingTest { + + @Test + fun `when grouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when ungrouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is not initialized then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.notInitializedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.sortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.unsortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.sortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.unsortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped but networks is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.UnableToSortTokenList.left() + + val useCase = getUseCase(networks = emptySet().right()) + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped but networks getting failed then error should be received`() = runTest { + // Given + val error = DataError.NetworkError.NoInternetConnection + val expectedResult = TokenListSortingError.DataError(error).left() + + val useCase = getUseCase(networks = error.left()) + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase(networks: Either> = MockNetworks.networks.right()) = + ToggleTokenListGroupingUseCase( + networksRepository = MockNetworksRepository(networks, statuses = flowOf()), + dispatchers = TestingCoroutineDispatcherProvider(), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt new file mode 100644 index 0000000000..f8070694f7 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt @@ -0,0 +1,143 @@ +package com.tangem.domain.tokens + +import arrow.core.left +import arrow.core.right +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class ToggleTokenListSortingUseCaseTest { + + @Test + fun `when grouped list is not initialized then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.notInitializedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when grouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when ungrouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase() = ToggleTokenListSortingUseCase( + dispatchers = TestingCoroutineDispatcherProvider(), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt new file mode 100644 index 0000000000..2e0aab633f --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.model.NetworkGroup + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworksGroups { + + val networkGroup1 = NetworkGroup( + network = MockNetworks.network1, + tokens = MockTokensStates.failedTokenStates + .filter { it.networkId == MockNetworks.network1.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup2 = NetworkGroup( + network = MockNetworks.network2, + tokens = MockTokensStates.failedTokenStates + .filter { it.networkId == MockNetworks.network2.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup3 = NetworkGroup( + network = MockNetworks.network3, + tokens = MockTokensStates.failedTokenStates + .filter { it.networkId == MockNetworks.network3.id } + .toNonEmptySetOrNull()!!, + ) + + val failedNetworksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) + + val loadedNetworksGroups = failedNetworksGroups.map { group -> + group.copy( + tokens = MockTokensStates.loadedTokensStates + .filter { it.networkId == group.network.id } + .toNonEmptySetOrNull()!!, + ) + }.toNonEmptySet() + + val sortedNetworksGroups = loadedNetworksGroups.map { group -> + group.copy( + tokens = group.tokens + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!!, + ) + } + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount!! } + } + .toNonEmptySetOrNull()!! +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index b2befbeeb1..604a2cbffc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -1,8 +1,10 @@ package com.tangem.domain.tokens.mock -import arrow.core.nonEmptySetOf +import arrow.core.NonEmptySet import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups +import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups +import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus import java.math.BigDecimal @@ -13,72 +15,59 @@ internal object MockTokenLists { const val isGrouped = false const val isSortedByBalance = false - val networkGroup1 = NetworkGroup( - network = MockNetworks.network1, - tokens = MockTokensStates.tokenStates - .filter { it.networkId == MockNetworks.network1.id } - .toNonEmptySetOrNull()!!, - ) - - val networkGroup2 = NetworkGroup( - network = MockNetworks.network2, - tokens = MockTokensStates.tokenStates - .filter { it.networkId == MockNetworks.network2.id } - .toNonEmptySetOrNull()!!, - ) - - val networkGroup3 = NetworkGroup( - network = MockNetworks.network3, - tokens = MockTokensStates.tokenStates - .filter { it.networkId == MockNetworks.network3.id } - .toNonEmptySetOrNull()!!, - ) - - val networksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) - - val sortedNetworksGroups = networksGroups.map { group -> - group.copy( - tokens = MockTokensStates.loadedTokensStates - .filter { it.networkId == group.network.id } - .sortedByDescending { it.value.fiatAmount } - .toNonEmptySetOrNull()!!, - ) - } - .sortedByDescending { group -> - group.tokens.sumOf { it.value.fiatAmount!! } - } - .toNonEmptySetOrNull()!! - val notInitializedTokenList = TokenList.NotInitialized - val groupedTokenList = TokenList.GroupedByNetwork( - groups = networksGroups, + val emptyGroupedTokenList = TokenList.GroupedByNetwork( + groups = emptySet(), totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) - val ungroupedTokenList = TokenList.Ungrouped( - tokens = MockTokensStates.tokenStates, + val emptyUngroupedTokenList = TokenList.Ungrouped( + tokens = emptySet(), totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) - val loadingUngroupedTokenList = with(ungroupedTokenList) { + val failedGroupedTokenList = TokenList.GroupedByNetwork( + groups = failedNetworksGroups, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val failedUngroupedTokenList = TokenList.Ungrouped( + tokens = MockTokensStates.failedTokenStates, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toSet(), + tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toNonEmptySetOrNull()!!, totalFiatBalance = TokenList.FiatBalance.Loading, ) } - val sortedUngroupedTokenList: TokenList.Ungrouped + val loadingGroupedTokenList = with(failedGroupedTokenList) { + copy( + totalFiatBalance = TokenList.FiatBalance.Loading, + groups = groups.map { group -> + group.copy( + tokens = group.tokens + .map { it.copy(value = TokenStatus.Loading) } + .toNonEmptySetOrNull()!!, + ) + }.toNonEmptySetOrNull()!!, + ) + } + + val unsortedUngroupedTokenList: TokenList.Ungrouped get() { val tokens = MockTokensStates.loadedTokensStates - .sortedByDescending { it.value.fiatAmount } - .toNonEmptySetOrNull()!! - return ungroupedTokenList.copy( + return failedUngroupedTokenList.copy( tokens = tokens, - sortedBy = TokenList.SortType.BALANCE, + sortedBy = TokenList.SortType.NONE, totalFiatBalance = TokenList.FiatBalance.Loaded( amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, @@ -86,19 +75,41 @@ internal object MockTokenLists { ) } - val sortedGroupedTokenList: TokenList.GroupedByNetwork + val unsortedGroupedTokenList: TokenList.GroupedByNetwork get() { - val groups = sortedNetworksGroups.toSet() + val groups = loadedNetworksGroups - return groupedTokenList.copy( + return failedGroupedTokenList.copy( groups = groups, - sortedBy = TokenList.SortType.BALANCE, + sortedBy = TokenList.SortType.NONE, totalFiatBalance = TokenList.FiatBalance.Loaded( amount = groups - .flatMap { it.tokens } + .flatMap { it.tokens as NonEmptySet } .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, ), ) } + + val sortedUngroupedTokenList: TokenList.Ungrouped + get() { + val tokens = MockTokensStates.loadedTokensStates + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!! + + return unsortedUngroupedTokenList.copy( + tokens = tokens, + sortedBy = TokenList.SortType.BALANCE, + ) + } + + val sortedGroupedTokenList: TokenList.GroupedByNetwork + get() { + val groups = sortedNetworksGroups + + return unsortedGroupedTokenList.copy( + groups = groups, + sortedBy = TokenList.SortType.BALANCE, + ) + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index ec1d422785..53703985ec 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -1,6 +1,5 @@ package com.tangem.domain.tokens.mock -import arrow.core.nonEmptySetOf import com.tangem.domain.tokens.model.Token internal object MockTokens { @@ -116,5 +115,5 @@ internal object MockTokens { derivationPath = null, ) - val tokens = nonEmptySetOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) + val tokens = setOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 9b5677d97e..b712bc465d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -117,7 +117,7 @@ internal object MockTokensStates { value = TokenStatus.NoAccount, ) - val tokenStates = nonEmptySetOf( + val failedTokenStates = nonEmptySetOf( tokenState1, tokenState2, tokenState3, @@ -130,7 +130,7 @@ internal object MockTokensStates { tokenState10, ) - val loadedTokensStates = tokenStates.map { state -> + val loadedTokensStates = failedTokenStates.map { state -> val networkStatus = MockNetworks.verifiedNetworksStatuses.first { it.networkId == state.networkId } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[state.id]!! val quote = MockQuotes.quotes.first { it.tokenId == state.id } @@ -144,5 +144,5 @@ internal object MockTokensStates { hasTransactionsInProgress = false, ), ) - } + }.toNonEmptySet() } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index c5db701f0b..02b76b06d5 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -9,17 +9,33 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map internal class MockTokensRepository( + private val sortTokensResult: Either, private val tokens: Flow>>, private val isGrouped: Flow>, private val isSortedByBalance: Flow>, ) : TokensRepository { - override suspend fun sortTokens( + var tokensIdsAfterSortingApply: Set? = null + private set + + var isTokensGroupedAfterSortingApply: Boolean? = null + private set + + var isTokensSortedByBalanceAfterSortingApply: Boolean? = null + private set + + override suspend fun saveTokens( userWalletId: UserWalletId, - sortedTokens: Set, - isGrouped: Boolean, + tokens: Set, + isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, - ) = Unit + ) { + sortTokensResult.onLeft { throw it } + + tokensIdsAfterSortingApply = tokens + isTokensGroupedAfterSortingApply = isGroupedByNetwork + isTokensSortedByBalanceAfterSortingApply = isSortedByBalance + } override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { return tokens.map { it.getOrElse { e -> throw e } } From e74f7adde9ca01360199cafe138526ef44335d2e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jul 2023 15:12:13 +0800 Subject: [PATCH 069/100] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tangem/tap/di/domain/CardDomainModule.kt | 25 ++++--- .../tap/di/domain/CardLegacyDomainModule.kt | 18 +++++ .../tangem/tap/features/demo/DemoHelper.kt | 73 +------------------ .../tap/features/demo/DemoMiddleware.kt | 12 +++ .../demo/DemoOnboardingNoteMiddleware.kt | 4 +- .../features/demo/DemoTransactionSender.kt | 51 +++++++++++++ domain/card/build.gradle.kts | 12 ++- domain/demo/.gitignore | 1 + domain/demo/build.gradle.kts | 13 ++++ .../com/tangem/domain}/demo/DemoConfig.kt | 6 +- .../tangem/domain/demo/IsDemoCardUseCase.kt | 6 ++ settings.gradle.kts | 3 +- 13 files changed, 133 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt create mode 100644 app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt create mode 100644 domain/demo/.gitignore create mode 100644 domain/demo/build.gradle.kts rename domain/{legacy/src/main/java/com/tangem/domain/common => demo/src/main/java/com/tangem/domain}/demo/DemoConfig.kt (98%) create mode 100644 domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0a8341a590..6e9c2b9786 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(project(":domain:models")) implementation(project(":domain:core")) implementation(projects.domain.card) + implementation(projects.domain.demo) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) implementation(projects.domain.settings) diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index d2f09c474f..a7f064da55 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -3,23 +3,20 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.IsDemoCardUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped @Module -@InstallIn(SingletonComponent::class) +@InstallIn(ViewModelComponent::class) internal object CardDomainModule { @Provides - @Singleton - fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() - - @Provides - @Singleton + @ViewModelScoped fun provideGetBiometricsStatusUseCase( cardSdkConfigRepository: CardSdkConfigRepository, ): GetBiometricsStatusUseCase { @@ -27,7 +24,7 @@ internal object CardDomainModule { } @Provides - @Singleton + @ViewModelScoped fun provideSetAccessCodeRequestPolicyUseCase( cardSdkConfigRepository: CardSdkConfigRepository, ): SetAccessCodeRequestPolicyUseCase { @@ -35,7 +32,7 @@ internal object CardDomainModule { } @Provides - @Singleton + @ViewModelScoped fun provideGetAccessCodeSavingStatusUseCase( cardSdkConfigRepository: CardSdkConfigRepository, ): GetAccessCodeSavingStatusUseCase { @@ -43,8 +40,12 @@ internal object CardDomainModule { } @Provides - @Singleton + @ViewModelScoped fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase { return GetCardWasScannedUseCase(cardRepository = cardRepository) } + + @Provides + @ViewModelScoped + fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt new file mode 100644 index 0000000000..3cfbe41b23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.card.* +import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardLegacyDomainModule { + + @Provides + @Singleton + fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index ef011c6dc1..fcf584cf31 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -1,18 +1,7 @@ package com.tangem.tap.features.demo -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionSender -import com.tangem.blockchain.common.TransactionSigner -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.toBlockchainSdkError -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchain.extensions.Result -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.common.CompletionResult +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.common.demo.DemoConfig import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -23,13 +12,6 @@ import com.tangem.tap.store import com.tangem.wallet.R import org.rekotlin.Action -/** -[REDACTED_AUTHOR] - */ -interface DemoMiddleware { - fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean -} - object DemoHelper { val config = DemoConfig() @@ -68,63 +50,10 @@ object DemoHelper { return false } - fun injectDemoBalance(walletManager: WalletManager?) { - val manager = walletManager ?: return - - val blockchain = walletManager.wallet.blockchain - val amount = config.getBalance(blockchain) - manager.wallet.setAmount(amount) - } - private fun getScanResponse(appState: () -> AppState?): ScanResponse? { val state = appState() ?: return null return state.globalState.onboardingState.onboardingManager?.scanResponse ?: state.globalState.scanResponse } -} - -class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { - - override suspend fun getFee(amount: Amount, destination: String): Result { - val blockchain = walletManager.wallet.blockchain - return Result.Success( - TransactionFee.Choosable( - minimum = Fee.Common(Amount(minimumFee, blockchain)), - normal = Fee.Common(Amount(normalFee, blockchain)), - priority = Fee.Common(Amount(priorityFee, blockchain)), - ), - ) - } - - @Suppress("MagicNumber") - override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { - val dataToSign = randomString(32).toByteArray() - val signerResponse = signer.sign( - hash = dataToSign, - publicKey = walletManager.wallet.publicKey, - ) - return when (signerResponse) { - is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) - is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) - } - } - - private fun randomInt(from: Int, to: Int): Int = kotlin.random.Random.nextInt(from, to) - - private fun randomString(length: Int): String { - val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - return (1..length) - .map { randomInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") - } - - companion object { - val ID = DemoTransactionSender::class.java.simpleName - - private val minimumFee = 0.0001.toBigDecimal() - private val normalFee = 0.0002.toBigDecimal() - private val priorityFee = 0.0003.toBigDecimal() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt new file mode 100644 index 0000000000..4f46273749 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.demo + +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +interface DemoMiddleware { + fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt index 4624db8d34..cb02be1422 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.common.demo.DemoConfig import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt new file mode 100644 index 0000000000..7e24b61c6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.features.demo + +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.common.CompletionResult +import kotlin.random.Random + +class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { + + override suspend fun getFee(amount: Amount, destination: String): Result { + val blockchain = walletManager.wallet.blockchain + return Result.Success( + TransactionFee.Choosable( + minimum = Fee.Common(Amount(minimumFee, blockchain)), + normal = Fee.Common(Amount(normalFee, blockchain)), + priority = Fee.Common(Amount(priorityFee, blockchain)), + ), + ) + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val signerResponse = signer.sign( + hash = getDataToSign(), + publicKey = walletManager.wallet.publicKey, + ) + return when (signerResponse) { + is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) + is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) + } + } + + private fun getDataToSign(): ByteArray { + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return IntRange(start = 1, endInclusive = 32) + .map { Random.nextInt(from = 0, until = charPool.size) } + .map(charPool::get) + .joinToString(separator = "") + .toByteArray() + } + + companion object { + val ID: String = DemoTransactionSender::class.java.simpleName + + private val minimumFee = 0.0001.toBigDecimal() + private val normalFee = 0.0002.toBigDecimal() + private val priorityFee = 0.0003.toBigDecimal() + } +} \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index a1266a8ba6..71512156cd 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -1,14 +1,22 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.card" +} + dependencies { + implementation(projects.core.analytics.models) + + implementation(projects.domain.demo) implementation(projects.domain.core) + implementation(projects.domain.legacy) // TODO: Remove after new card scan result was implemented implementation(projects.domain.models) - implementation(projects.core.analytics.models) implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/domain/demo/.gitignore b/domain/demo/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/demo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/demo/build.gradle.kts b/domain/demo/build.gradle.kts new file mode 100644 index 0000000000..f0c56e2a73 --- /dev/null +++ b/domain/demo/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.demo" +} + +dependencies { + implementation(deps.tangem.blockchain) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt similarity index 98% rename from domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt rename to domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt index 670e104425..6c22a84135 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt @@ -1,8 +1,8 @@ -package com.tangem.domain.common.demo +package com.tangem.domain.demo +import com.tangem.blockchain.BuildConfig import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.features.BuildConfig import java.math.BigDecimal @Suppress("LargeClass") @@ -15,7 +15,7 @@ class DemoConfig { Blockchain.Solana, ) - val demoCardIds: List by lazy { + private val demoCardIds: List by lazy { val demoIds = getReleaseIds().toMutableList() if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds) diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt b/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt new file mode 100644 index 0000000000..209d7984ba --- /dev/null +++ b/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.demo + +class IsDemoCardUseCase(private val config: DemoConfig) { + + operator fun invoke(cardId: String): Boolean = config.isDemoCardId(cardId) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 675e2f1073..15622f6a47 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -77,8 +77,9 @@ include(":features:learn2earn:impl") include(":domain:models") include(":domain:legacy") -include(":domain:core") include(":domain:card") +include(":domain:core") +include(":domain:demo") include(":domain:settings") include(":domain:tokens") include(":domain:wallets") From 1dec124c0ff4fc77e10bfce635441323710c9b68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 18:45:11 +0400 Subject: [PATCH 070/100] Updated on 2026-08-14 --- .../core/ui/extensions/BlockchainIcons.kt | 2 + .../main/res/drawable/img_octaspace_22.xml | 215 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 core/ui/src/main/res/drawable/img_octaspace_22.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index c4eeba732a..c5e8a5e327 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -42,6 +42,7 @@ fun getActiveIconRes(blockchainId: String): Int { "cronos" -> R.drawable.img_cronos_22 "TELOS", "TELOS/test" -> R.drawable.img_telos_22 "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 + "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 else -> R.drawable.ic_alert_24 } } @@ -87,6 +88,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int { "terra" -> R.drawable.img_terra_22 "terra-2" -> R.drawable.img_terra2_22 "telos" -> R.drawable.img_telos_22 + "octaspace" -> R.drawable.img_octaspace_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_octaspace_22.xml b/core/ui/src/main/res/drawable/img_octaspace_22.xml new file mode 100644 index 0000000000..21a7f76d64 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_octaspace_22.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6171aa8097ef25010ba92b6cd7776f9a7f60870e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 10:06:30 +0300 Subject: [PATCH 071/100] Updated on 2026-08-14 --- .../datasource/files/AndroidFileReader.kt | 11 ++- .../local/datastore/FileDataStore.kt | 19 ++-- .../local/datastore/model/WriteTrigger.kt | 3 + data/tokens/build.gradle.kts | 20 ++++- .../data/tokens/utils/CardTokensFactory.kt | 89 +++++++++++++++++++ .../tokens/utils/ResponseTokensFactory.kt | 58 ++++++++++++ .../data/tokens/utils/TokensOperations.kt | 75 ++++++++++++++++ .../tokens/utils/UserTokensResponseFactory.kt | 44 +++++++++ .../java/com/tangem/domain/demo/DemoConfig.kt | 1 + .../com/tangem/domain/models/scan/CardDTO.kt | 1 + .../tangem/domain/models/scan/ScanResponse.kt | 1 + 11 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt index 8ed8eb243b..01b45b3a15 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt @@ -5,13 +5,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject class AndroidFileReader @Inject constructor(@ApplicationContext private val context: Context) : FileReader { + override fun readFile(fileName: String): String { - return context.openFileInput(fileName).bufferedReader().readText() + return context.openFileInput(fileName).use { stream -> + stream.bufferedReader().use { reader -> + reader.readText() + } + } } override fun rewriteFile(content: String, fileName: String) { - context.openFileOutput(fileName, Context.MODE_PRIVATE).use { - it.write(content.toByteArray(), 0, content.length) + context.openFileOutput(fileName, Context.MODE_PRIVATE).use { stream -> + stream.write(content.toByteArray(), 0, content.length) } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index 2da02a5df8..aa89c11b91 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -2,12 +2,11 @@ package com.tangem.datasource.local.datastore import com.squareup.moshi.JsonAdapter import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.datastore.model.WriteTrigger import com.tangem.domain.core.error.DataError import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import java.io.FileNotFoundException internal class FileDataStore( private val fileNameProvider: (key: String) -> String, @@ -15,12 +14,14 @@ internal class FileDataStore( private val adapter: JsonAdapter, ) { - private val writeTrigger = MutableSharedFlow( + private val writeTrigger = MutableSharedFlow( + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) fun get(key: String): Flow { return writeTrigger + .onEmpty { emit(WriteTrigger) } .map { getInternal(fileNameProvider(key)) } .filterNotNull() } @@ -34,7 +35,7 @@ internal class FileDataStore( val json = adapter.toJson(content) fileReader.rewriteFile(json, fileNameProvider(key)) - writeTrigger.tryEmit(Unit) + writeTrigger.tryEmit(WriteTrigger) } catch (e: Throwable) { throw DataError.PersistenceError.UnableToWriteFile(e) } @@ -42,7 +43,11 @@ internal class FileDataStore( private fun getInternal(fileName: String): Data? { return try { - val json = fileReader.readFile(fileName) + val json = try { + fileReader.readFile(fileName) + } catch (e: FileNotFoundException) { + return null + } adapter.fromJson(json) } catch (e: Throwable) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt new file mode 100644 index 0000000000..aaab7f83df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.local.datastore.model + +internal typealias WriteTrigger = Unit \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 2d8aaaf35d..9fa03694fe 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -1,18 +1,34 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) id("configuration") } +android { + namespace = "com.tangem.data.tokens" +} + dependencies { implementation(projects.domain.core) + implementation(projects.domain.models) implementation(projects.domain.tokens) + implementation(projects.domain.demo) implementation(projects.domain.wallets.models) - implementation(deps.kotlin.coroutines) + implementation(projects.core.datasource) + // FIXME: For blockchain extensions, remove after refactoring + implementation(projects.domain.legacy) + + implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.hilt.core) kapt(deps.hilt.kapt) + + implementation(deps.timber) + + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt new file mode 100644 index 0000000000..671810e7e7 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -0,0 +1,89 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.Network +import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.tokens.model.Token as DomainToken + +@Suppress("unused") // TODO: Will be used in next MR +internal class CardTokensFactory(private val demoConfig: DemoConfig) { + + fun createDefaultTokensForMultiCurrencyCard(card: CardDTO): Set { + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { createCoin(it, card) }.toSet() + } + + fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set { + val card = scanResponse.card + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = requireNotNull(createCoin(blockchain, card)) { + "Coin for the single currency card cannot be null" + } + val primaryToken = resolver.getPrimaryToken()?.let { token -> + createToken(token, blockchain, card) + } + + return buildSet { + add(coin) + + if (primaryToken != null) { + add(primaryToken) + } + } + } + + private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? { + if (blockchain != Blockchain.Unknown) { + Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + return null + } + + return DomainToken( + id = getTokenId(sdkToken, blockchain), + networkId = Network.ID(blockchain.id), + name = sdkToken.name, + symbol = sdkToken.symbol, + iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain), + decimals = sdkToken.decimals, + isCustom = false, + contractAddress = sdkToken.contractAddress, + derivationPath = getDerivationPath(blockchain, card), + ) + } + + private fun createCoin(blockchain: Blockchain, card: CardDTO): DomainToken? { + if (blockchain != Blockchain.Unknown) { + Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + return null + } + + return DomainToken( + id = getCoinId(blockchain), + networkId = Network.ID(blockchain.id), + name = blockchain.fullName, + symbol = blockchain.currency, + iconUrl = getCoinIconId(blockchain), + decimals = blockchain.decimals(), + isCustom = false, + contractAddress = null, + derivationPath = getDerivationPath(blockchain, card), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt new file mode 100644 index 0000000000..462a115635 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.Token +import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken + +@Suppress("unused") // TODO: Will be used in next MR +internal class ResponseTokensFactory(private val demoConfig: DemoConfig) { + + fun createTokens(response: UserTokensResponse, card: CardDTO): Set { + return response.tokens.mapNotNull { createToken(it, card) }.toSet() + } + + private fun createToken(token: UserTokensResponse.Token, card: CardDTO): Token? { + var blockchain = Blockchain.fromNetworkId(token.networkId) + if (blockchain == null) { + Timber.e("Unable to find a blockchain with the network ID: ${token.networkId}") + return null + } + + if (demoConfig.isDemoCardId(card.cardId)) { + blockchain = blockchain.getTestnetVersion() ?: blockchain + } + + val sdkToken = createSdkToken(token) + + return Token( + id = getTokenId(sdkToken, blockchain), + networkId = Network.ID(blockchain.id), + name = token.name, + symbol = token.symbol, + decimals = token.decimals, + iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain), + contractAddress = token.contractAddress, + derivationPath = token.derivationPath, + isCustom = isCustomToken(token.id, token.derivationPath, card.derivationStyle, blockchain), + ) + } + + private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? { + return token.contractAddress?.let { contractAddress -> + SdkToken( + name = token.name, + symbol = token.symbol, + contractAddress = contractAddress, + decimals = token.decimals, + id = token.id, + ) + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt new file mode 100644 index 0000000000..72938103a5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -0,0 +1,75 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.IconsUtil +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.Token +import com.tangem.blockchain.common.Token as SdkToken + +private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" +private const val TOKEN_ICON_SIZE = "large" +private const val TOKEN_ICON_EXT = "png" + +internal fun isCustomToken( + tokenId: String?, + tokenDerivationPath: String?, + cardDerivationStyle: DerivationStyle?, + blockchain: Blockchain, +): Boolean { + if (tokenId == null) return true + + if (tokenDerivationPath == null || cardDerivationStyle == null) return false + + return tokenDerivationPath != blockchain.derivationPath(cardDerivationStyle)?.rawPath +} + +internal fun getTokenId(token: SdkToken?, blockchain: Blockchain): Token.ID { + val tokenId = token?.id + + return when { + token == null -> getCoinId(blockchain) + tokenId == null -> getCustomTokenId(token.contractAddress, blockchain) + else -> Token.ID(tokenId) + } +} + +internal fun getCoinId(blockchain: Blockchain): Token.ID { + val value = blockchain.toCoinId() + + return Token.ID(value) +} + +internal fun getCoinOrTokenIconUrl(token: SdkToken?, blockchain: Blockchain): String? { + val tokenId = token?.id + + return when { + token == null -> getCoinIconId(blockchain) + tokenId == null -> IconsUtil.getTokenIconUri(blockchain, token)?.toString() + else -> getTokenIconUrlFromDefaultHost(tokenId) + } +} + +internal fun getCoinIconId(blockchain: Blockchain): String { + return getTokenIconUrlFromDefaultHost(blockchain.toCoinId()) +} + +internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? { + return if (card.settings.isHDWalletAllowed) { + blockchain.derivationPath(card.derivationStyle)?.rawPath + } else { + null + } +} + +private fun getCustomTokenId(contractAddress: String, blockchain: Blockchain): Token.ID { + val value = "custom_${blockchain.id}_$contractAddress" + + return Token.ID(value) +} + +private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { + return "$DEFAULT_TOKENS_ICONS_HOST/$TOKEN_ICON_SIZE/$tokenId.$TOKEN_ICON_EXT" +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt new file mode 100644 index 0000000000..6c895bee65 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -0,0 +1,44 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.tokens.model.Token + +@Suppress("unused") // TODO: Will be used in next MR +internal class UserTokensResponseFactory { + + fun createUserTokensResponse( + tokens: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): UserTokensResponse { + return UserTokensResponse( + tokens = tokens.map(::createResponseToken), + group = if (isGroupedByNetwork) { + UserTokensResponse.GroupType.NETWORK + } else { + UserTokensResponse.GroupType.NONE + }, + sort = if (isSortedByBalance) { + UserTokensResponse.SortType.BALANCE + } else { + UserTokensResponse.SortType.MANUAL + }, + ) + } + + private fun createResponseToken(domainToken: Token): UserTokensResponse.Token { + val blockchain = Blockchain.fromId(domainToken.networkId.value) + + return UserTokensResponse.Token( + id = domainToken.id.value.takeUnless { domainToken.isCustom }, + networkId = blockchain.toNetworkId(), + derivationPath = domainToken.derivationPath, + name = domainToken.name, + symbol = domainToken.symbol, + decimals = domainToken.decimals, + contractAddress = domainToken.contractAddress, + ) + } +} \ No newline at end of file diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt index 6c22a84135..ad6e89bba0 100644 --- a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import java.math.BigDecimal +// FIXME: Move to :domain:demo:models @Suppress("LargeClass") class DemoConfig { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index a89909b584..1777aa498a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -11,6 +11,7 @@ import com.tangem.operations.attestation.Attestation import java.util.* import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion +// TODO: Move to :domain:card:models /** * [Card] copy * */ diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 352a0564f1..c6f1d9f75c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -5,6 +5,7 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.ExtendedPublicKeysMap +// TODO: Move to :domain:card:models /** [REDACTED_AUTHOR] */ From 3e1a45dbad3845803e289dd099f0bc52e75143f0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jul 2023 18:22:26 +0300 Subject: [PATCH 072/100] Updated on 2026-08-14 --- .../converters/ShopOrderToEventConverter.kt | 2 +- .../tap/common/shop/TangemShopService.kt | 9 +--- .../tap/common/shop/data/ProductType.kt | 19 ------- .../tap/common/shop/data/TangemProduct.kt | 2 + .../shop/data/DefaultShopRepository.kt | 24 ++++++++- .../shop/data/SalesProductConverter.kt | 40 ++++++++++++++ .../tap/features/shop/di/ShopUseCaseModule.kt | 26 +++++++-- .../features/shop/di/ShopifyTogglesModule.kt | 23 ++++++++ .../domain/GetShopifySalesProductsUseCase.kt | 25 +++++++++ .../features/shop/domain/ShopRepository.kt | 7 ++- .../shop/domain/models/ProductType.kt | 22 ++++++++ .../features/shop/domain/models/SalesError.kt | 5 ++ .../shop/domain/models/SalesProduct.kt | 30 +++++++++++ .../shop/presentation/ShopViewModel.kt | 21 ++++++++ .../tap/features/shop/redux/ShopAction.kt | 7 ++- .../tap/features/shop/redux/ShopReducer.kt | 6 +++ .../tap/features/shop/redux/ShopState.kt | 4 +- .../DefaultShopifyFeatureToggleManager.kt | 11 ++++ .../toggles/ShopifyFeatureToggleManager.kt | 10 ++++ .../tap/features/shop/ui/ShopFragment.kt | 53 +++++++++++++++++-- .../api/tangemTech/TangemTechApi.kt | 6 +++ .../api/tangemTech/models/SalesResponse.kt | 44 +++++++++++++++ .../configs/feature_toggles_config.json | 4 ++ 23 files changed, 360 insertions(+), 40 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt create mode 100644 app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt index 5d779e661f..6b8f903fb9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt @@ -3,7 +3,7 @@ package com.tangem.tap.common.analytics.converters import com.shopify.buy3.Storefront import com.tangem.common.Converter import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index 3032ef611a..97d9bd093b 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -8,12 +8,13 @@ import com.tangem.core.analytics.Analytics import com.tangem.datasource.config.models.ShopifyShop import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct import com.tangem.tap.common.shop.data.TotalSum import com.tangem.tap.common.shop.googlepay.GooglePayService import com.tangem.tap.common.shop.shopify.ShopifyService import com.tangem.tap.common.shop.shopify.data.CheckoutItem +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -206,12 +207,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { } } } - - companion object { - const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" - const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" - val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) - } } private fun Storefront.MoneyV2.format(): String { diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt deleted file mode 100644 index 4238b4d883..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.common.shop.data - -import com.tangem.tap.common.shop.TangemShopService - -enum class ProductType(val sku: String) { - WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU), - WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU), - ; - - companion object { - fun fromSku(sku: String): ProductType? { - return when (sku) { - WALLET_2_CARDS.sku -> WALLET_2_CARDS - WALLET_3_CARDS.sku -> WALLET_3_CARDS - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt index 79eb72e1f1..9a01a5df51 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt @@ -1,5 +1,7 @@ package com.tangem.tap.common.shop.data +import com.tangem.tap.features.shop.domain.models.ProductType + data class TangemProduct( val type: ProductType, val totalSum: TotalSum? = null, diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt index 74505b69d3..1abbd3d17c 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt @@ -2,10 +2,13 @@ package com.tangem.tap.features.shop.data import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.ShopResponse +import com.tangem.domain.common.extensions.withIOContext import com.tangem.tap.features.shop.domain.ShopRepository +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import timber.log.Timber +import java.util.Locale /** * Default implementation of shop feature repository @@ -20,6 +23,8 @@ internal class DefaultShopRepository( private val dispatchers: CoroutineDispatcherProvider, ) : ShopRepository { + private val salesProductConverter = SalesProductConverter() + override suspend fun isShopifyOrderingAvailable(): Boolean { return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) } .fold( @@ -31,7 +36,24 @@ internal class DefaultShopRepository( ) } + override suspend fun getSalesProductInfo(): List { + return withIOContext { + val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME) + salesProductConverter.convert(salesInfo) + } + } + + private fun getLocaleName(): String { + return if (Locale.getDefault().language == "ru") { + RU_LOCALE + } else { + EN_LOCALE + } + } + private companion object { - const val SHOPIFY_NAME = "shopify" + private const val SHOPIFY_NAME = "shopify" + private const val RU_LOCALE = "ru" + private const val EN_LOCALE = "en" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt new file mode 100644 index 0000000000..9d8f440da6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.features.shop.data + +import com.tangem.datasource.api.tangemTech.models.SalesResponse +import com.tangem.tap.features.shop.domain.models.Notification +import com.tangem.tap.features.shop.domain.models.ProductState +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.SalesProduct +import com.tangem.utils.converter.Converter + +internal class SalesProductConverter : Converter> { + + override fun convert(value: SalesResponse): List { + return value.sales.map { sales -> + val productState = when (sales.state) { + "order" -> ProductState.ORDER + "pre-order" -> ProductState.PRE_ORDER + "sold-out" -> ProductState.SOLD_OUT + else -> ProductState.SOLD_OUT + } + val productType = when (sales.product.code) { + "pack2" -> ProductType.WALLET_2_CARDS + "pack3" -> ProductType.WALLET_3_CARDS + else -> ProductType.WALLET_3_CARDS + } + SalesProduct( + id = sales.id, + productType = productType, + state = productState, + name = sales.product.name, + notification = sales.notification?.let { notification -> + Notification( + type = notification.type, + title = notification.title, + description = notification.description, + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt index 2c00aa1181..a6f22bec79 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.features.shop.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.tap.features.shop.data.DefaultShopRepository import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase +import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase +import com.tangem.tap.features.shop.domain.ShopRepository import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -17,12 +19,26 @@ internal object ShopUseCaseModule { @Provides @ViewModelScoped - fun provideShopifyOrderingAvailabilityUseCase( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - ): ShopifyOrderingAvailabilityUseCase { + fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase { return DefaultShopifyOrderingAvailabilityUseCase( - shopRepository = DefaultShopRepository(tangemTechApi, dispatchers), + shopRepository = shopRepository, ) } + + @Provides + @ViewModelScoped + fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase { + return GetShopifySalesProductsUseCase( + shopRepository = shopRepository, + ) + } + + @Provides + @ViewModelScoped + fun provideDefaultShopRepository( + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): ShopRepository { + return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt new file mode 100644 index 0000000000..97b5839f47 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.features.shop.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager +import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ShopifyTogglesModule { + + @Provides + @Singleton + fun provideDefaultShopifyFeatureToggleManager( + featureToggleManager: FeatureTogglesManager, + ): ShopifyFeatureToggleManager { + return DefaultShopifyFeatureToggleManager(featureToggleManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt new file mode 100644 index 0000000000..7881b82dea --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.features.shop.domain + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.tap.features.shop.domain.models.SalesError +import com.tangem.tap.features.shop.domain.models.SalesProduct + +/** + * Use case to get shopify available products + * + * @property shopRepository shop feature repository + */ +class GetShopifySalesProductsUseCase( + private val shopRepository: ShopRepository, +) { + + suspend operator fun invoke(): Either> { + return try { + shopRepository.getSalesProductInfo().right() + } catch (e: Exception) { + SalesError.DataError(e).left() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt index c8cd6f3780..67803630b8 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt @@ -1,12 +1,17 @@ package com.tangem.tap.features.shop.domain +import com.tangem.tap.features.shop.domain.models.SalesProduct + /** * Shop feature repository * [REDACTED_AUTHOR] */ -internal interface ShopRepository { +interface ShopRepository { /** Get shopify ordering availability */ suspend fun isShopifyOrderingAvailable(): Boolean + + /** Get actual sales product info */ + suspend fun getSalesProductInfo(): List } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt new file mode 100644 index 0000000000..0987fdd8bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.features.shop.domain.models + +private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" +private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" + +enum class ProductType(val sku: String) { + + WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU), + WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU), + ; + + companion object { + val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) + fun fromSku(sku: String): ProductType? { + return when (sku) { + WALLET_2_CARDS.sku -> WALLET_2_CARDS + WALLET_3_CARDS.sku -> WALLET_3_CARDS + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt new file mode 100644 index 0000000000..373b8d14c8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.shop.domain.models + +sealed class SalesError { + data class DataError(val cause: Throwable) : SalesError() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt new file mode 100644 index 0000000000..b3b5a083d3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.shop.domain.models + +/** + * Sales product + * + * @property id product id + * @property productType shows TW2 cards or 3 cards + * @property state state as order available etc + * @property name product name + * @property notification optional notification + */ +data class SalesProduct( + val id: String, + val productType: ProductType, + val state: ProductState, + val name: String, + val notification: Notification?, +) + +data class Notification( + val type: String, + val title: String, + val description: String, +) + +enum class ProductState { + ORDER, + SOLD_OUT, + PRE_ORDER, +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt index 34966466cc..2e39e2bd4f 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.shop.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.proxy.AppStateHolder @@ -15,6 +16,7 @@ import javax.inject.Inject * Shop screen view model * * @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability + * @property getShopifySalesProductsUseCase use case to get actual sales info * @property dispatchers coroutine dispatchers provider * @property appStateHolder redux state holder * @@ -23,6 +25,7 @@ import javax.inject.Inject @HiltViewModel internal class ShopViewModel @Inject constructor( private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase, + private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase, private val dispatchers: CoroutineDispatcherProvider, private val appStateHolder: AppStateHolder, ) : ViewModel() { @@ -36,4 +39,22 @@ internal class ShopViewModel @Inject constructor( appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility)) } } + + /** + * Get actual sales products info + * to configure view dynamically + */ + fun getActualSalesInfo() { + viewModelScope.launch(dispatchers.main) { + val action = getShopifySalesProductsUseCase().fold( + ifLeft = { + ShopAction.SalesProductsError + }, + ifRight = { + ShopAction.SalesProductsLoaded(it) + }, + ) + appStateHolder.mainStore?.dispatch(action) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt index 54c6bcad43..a72324e845 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -2,9 +2,10 @@ package com.tangem.tap.features.shop.redux import android.content.Intent import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType import com.tangem.tap.common.shop.data.TangemProduct import com.tangem.tap.common.shop.googlepay.GooglePayService +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.wallet.R import org.rekotlin.Action @@ -44,4 +45,8 @@ sealed interface ShopAction : Action { object ResetState : ShopAction data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction + + data class SalesProductsLoaded(val salesProducts: List) : ShopAction + + object SalesProductsError : ShopAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt index 60cb0667ad..11061c7abf 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -39,5 +39,11 @@ private fun internalReduce(action: Action, state: ShopState): ShopState { is ShopAction.FinishSuccessfulOrder, is ShopAction.LoadProducts.Failure, -> state + is ShopAction.SalesProductsLoaded -> state.copy( + salesProducts = action.salesProducts, + ) + is ShopAction.SalesProductsError -> state.copy( + salesProducts = emptyList(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt index 491e961e58..08bac48f4d 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt @@ -1,12 +1,14 @@ package com.tangem.tap.features.shop.redux -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType import com.tangem.tap.common.shop.data.TangemProduct +import com.tangem.tap.features.shop.domain.models.SalesProduct import org.rekotlin.StateType data class ShopState( val availableProducts: List = emptyList(), val selectedProduct: ProductType = ProductType.WALLET_3_CARDS, + val salesProducts: List = emptyList(), val promoCode: String? = null, val promoCodeLoading: Boolean = false, val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt new file mode 100644 index 0000000000..eda902f707 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.shop.toggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultShopifyFeatureToggleManager( + private val featureTogglesManager: FeatureTogglesManager, +) : ShopifyFeatureToggleManager { + + override val isDynamicSalesProductsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt new file mode 100644 index 0000000000..ab3b8fc968 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.shop.toggles + +/** + * Shopify feature toggle manager that provides info about shopify toggle availability + * + */ +interface ShopifyFeatureToggleManager { + + val isDynamicSalesProductsEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt index 4af0db0285..58b89b9901 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -17,20 +17,27 @@ import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.shop.domain.models.ProductState +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.tap.features.shop.presentation.ShopViewModel import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.shop.redux.ShopState +import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentShopBinding import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject @AndroidEntryPoint internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { + @Inject + lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager + private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) private var cardTranslationY = 70f @@ -50,7 +57,11 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel.checkOrderingDelayBlockVisibility() + if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { + viewModel.getActualSalesInfo() + } else { + viewModel.checkOrderingDelayBlockVisibility() + } activity?.onBackPressedDispatcher?.addCallback( this, @@ -142,7 +153,11 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu animateProductSelection(state.selectedProduct) handlePriceState(state) handlePromoCodeState(state) - handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) + if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { + handleNotificationBlock(state) + } else { + handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) + } handleButtonsState(state) } @@ -187,6 +202,17 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() } + private fun handleNotificationBlock(state: ShopState) { + if (isVisible) { + binding.tvSoldOutDesc.show() + getSelectedSalesProduct(state)?.notification?.let { notification -> + binding.tvSoldOutDesc.text = notification.description + } + } else { + binding.tvSoldOutDesc.hide() + } + } + private fun handleButtonsState(state: ShopState) = with(binding) { btnPayGooglePay.root.show(state.isGooglePayAvailable) btnAlternativePayment.show(state.isGooglePayAvailable) @@ -194,8 +220,15 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu if (state.total != null) { btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) } + btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + if (state.salesProducts.isNotEmpty()) { + getSelectedSalesProduct(state)?.let { selectedProduct -> + btnMainAction.text = getMainBtnTextByProductState( + productState = selectedProduct.state, + ) + } + } } } @@ -203,4 +236,16 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu store.dispatch(ShopAction.ResetState) super.handleOnBackPressed() } + + private fun getSelectedSalesProduct(state: ShopState): SalesProduct? { + return state.salesProducts.find { + it.productType == state.selectedProduct + } + } + + private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) { + ProductState.ORDER -> getString(R.string.shop_buy_now) + ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR + ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 992f6d0eb5..e75594060f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -54,4 +54,10 @@ interface TangemTechApi { @GET("shops") suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse + + @GET("sales") + suspend fun getSalesInfo( + @Query(value = "locale") locale: String, + @Query(value = "shops") shops: String, + ): SalesResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt new file mode 100644 index 0000000000..1c3461f012 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +/** + * Sales response + */ +data class SalesResponse( + @Json(name = "sales") val sales: List, +) + +/** + * Sales info + * + * @property id sales id + * @property state state as order, sold-out, pre-order + * @property product product that is sales + * @property notification optional notification for product + */ +data class Sales( + @Json(name = "id") val id: String, + @Json(name = "state") val state: String, + @Json(name = "product") val product: Product, + @Json(name = "notification") val notification: Notification?, +) + +/** + * Product + * + * @property id product id + * @property code code that shows what product it is + * @property name product name + */ +data class Product( + @Json(name = "id") val id: String, + @Json(name = "code") val code: String, + @Json(name = "name") val name: String, +) + +data class Notification( + @Json(name = "type") val type: String, + @Json(name = "title") val title: String, + @Json(name = "description") val description: String, +) \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index b5005743ed..b2d4da3602 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -22,5 +22,9 @@ { "name": "REDESIGNED_TOKEN_DETAIL_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "SHOPIFY_DYNAMIC_ENABLED", + "version": "undefined" } ] From 60169334807f52edf2d1e872cf7e51310cd1b836 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jul 2023 12:54:28 +0300 Subject: [PATCH 073/100] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 4 ++-- .../datasource/local/cache/CacheKeysStore.kt | 2 +- .../local/cache/RuntimeCacheKeysStore.kt | 4 ++-- .../local/datastore/FileDataStore.kt | 22 +++++++++---------- .../local/datastore/RuntimeDataStore.kt | 2 +- .../local/token/FileUserTokensStore.kt | 4 ++-- .../datasource/local/token/UserTokensStore.kt | 2 +- .../local/userwallet/UserWalletsStore.kt | 2 +- .../data/common/cache/DefaultCacheRegistry.kt | 2 +- 9 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 7542283107..9b2167ff12 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -12,10 +12,10 @@ internal class RuntimeUserWalletsStore( private val walletsStateHolder: WalletsStateHolder, ) : UserWalletsStore { - override suspend fun getSync(userWalletId: UserWalletId): UserWallet? { + override suspend fun getOrNull(userWalletId: UserWalletId): UserWallet? { return walletsStateHolder.userWalletsListManager ?.userWallets ?.firstOrNull() - ?.firstOrNull { it.walletId == userWalletId } + ?.singleOrNull { it.walletId == userWalletId } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt index fc32a2b16e..1283fdd97d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.local.cache.model.CacheKey interface CacheKeysStore { - fun get(id: String): CacheKey? + fun getOrNull(id: String): CacheKey? fun addOrReplace(key: CacheKey) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt index 80bce89b0d..61ec6ce85c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt @@ -7,8 +7,8 @@ internal class RuntimeCacheKeysStore : CacheKeysStore { private val store = RuntimeDataStore(keyProvider = CacheKey::id) - override fun get(id: String): CacheKey? { - return store.getSync { it.id == id }.firstOrNull() + override fun getOrNull(id: String): CacheKey? { + return store.getSync { it.id == id }.singleOrNull() } override fun addOrReplace(key: CacheKey) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index aa89c11b91..dc647aad1c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -3,12 +3,11 @@ package com.tangem.datasource.local.datastore import com.squareup.moshi.JsonAdapter import com.tangem.datasource.files.FileReader import com.tangem.datasource.local.datastore.model.WriteTrigger -import com.tangem.domain.core.error.DataError import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* -import java.io.FileNotFoundException +import timber.log.Timber -internal class FileDataStore( +internal class FileDataStore( private val fileNameProvider: (key: String) -> String, private val fileReader: FileReader, private val adapter: JsonAdapter, @@ -26,32 +25,31 @@ internal class FileDataStore( .filterNotNull() } - fun getSync(key: String): Data? { + fun getSyncOrNull(key: String): Data? { return getInternal(fileNameProvider(key)) } fun store(key: String, content: Data) { + val fileName = fileNameProvider(key) + try { val json = adapter.toJson(content) - fileReader.rewriteFile(json, fileNameProvider(key)) + fileReader.rewriteFile(json, fileName) writeTrigger.tryEmit(WriteTrigger) } catch (e: Throwable) { - throw DataError.PersistenceError.UnableToWriteFile(e) + Timber.e(e, "Unable to write file: $fileName") } } private fun getInternal(fileName: String): Data? { return try { - val json = try { - fileReader.readFile(fileName) - } catch (e: FileNotFoundException) { - return null - } + val json = fileReader.readFile(fileName) adapter.fromJson(json) } catch (e: Throwable) { - throw DataError.PersistenceError.UnableToReadFile(e) + Timber.e(e, "Unable to read file: $fileName") + null } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index ba55ef6039..fced00f858 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -internal class RuntimeDataStore(private val keyProvider: (Data) -> Key) { +internal class RuntimeDataStore(private val keyProvider: (Data) -> Key) { private val store = MutableStateFlow>(hashMapOf()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt index 1df1f44428..5b12b188de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt @@ -18,8 +18,8 @@ internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserT return store.get(userWalletId) } - override suspend fun getSync(userWalletId: String): UserTokensResponse? { - return store.getSync(userWalletId) + override suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? { + return store.getSyncOrNull(userWalletId) } override suspend fun store(userWalletId: String, tokens: UserTokensResponse) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt index befab5e661..43f2f1e64a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -7,7 +7,7 @@ interface UserTokensStore { fun get(userWalletId: String): Flow - suspend fun getSync(userWalletId: String): UserTokensResponse? + suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? suspend fun store(userWalletId: String, tokens: UserTokensResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 9b4e2693b7..4b47934b9a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,5 +5,5 @@ import com.tangem.domain.wallets.models.UserWalletId interface UserWalletsStore { - suspend fun getSync(userWalletId: UserWalletId): UserWallet? + suspend fun getOrNull(userWalletId: UserWalletId): UserWallet? } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 1aabdd2e77..47bfbc3675 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -10,7 +10,7 @@ internal class DefaultCacheRegistry( ) : CacheRegistry { override suspend fun isExpired(key: String): Boolean { - val cacheKey = cacheKeysStore.get(key) ?: return true + val cacheKey = cacheKeysStore.getOrNull(key) ?: return true return cacheKey.updatedAt .plus(cacheKey.expiresIn) From ef19a3697d8d70d6d1c9705a2a8150145e63846e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 12:15:26 +0300 Subject: [PATCH 074/100] Updated on 2026-08-14 --- .../local/token/FileUserTokensStore.kt | 13 +- .../datasource/local/token/UserTokensStore.kt | 7 +- data/tokens/build.gradle.kts | 20 ++- .../tangem/data/tokens/di/TokensDataModule.kt | 17 +- .../com/tangem/data/tokens/mock/MockTokens.kt | 33 ++-- .../repository/DefaultTokensRepository.kt | 151 ++++++++++++++++++ .../tokens/repository/MockTokensRepository.kt | 30 ---- .../data/tokens/utils/CardTokensFactory.kt | 1 - .../tokens/utils/ResponseTokensFactory.kt | 1 - .../tokens/utils/UserTokensResponseFactory.kt | 1 - 10 files changed, 202 insertions(+), 72 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt index 5b12b188de..3c6e41fd4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.files.FileReader import com.tangem.datasource.local.datastore.FileDataStore +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserTokensStore { @@ -14,15 +15,15 @@ internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserT adapter = moshi.adapter(UserTokensResponse::class.java), ) - override fun get(userWalletId: String): Flow { - return store.get(userWalletId) + override fun get(userWalletId: UserWalletId): Flow { + return store.get(userWalletId.stringValue) } - override suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? { - return store.getSyncOrNull(userWalletId) + override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { + return store.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(userWalletId: String, tokens: UserTokensResponse) { - store.store(userWalletId, tokens) + override suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) { + store.store(userWalletId.stringValue, tokens) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt index 43f2f1e64a..8b64c93767 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -1,13 +1,14 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface UserTokensStore { - fun get(userWalletId: String): Flow + fun get(userWalletId: UserWalletId): Flow - suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? + suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? - suspend fun store(userWalletId: String, tokens: UserTokensResponse) + suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) } \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 9fa03694fe..3a9ee305bf 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -10,25 +10,35 @@ android { } dependencies { + + /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.demo) implementation(projects.domain.wallets.models) + /** Project - Data */ implementation(projects.core.datasource) + implementation(projects.data.common) + /** Project - Utils */ + implementation(projects.core.utils) // FIXME: For blockchain extensions, remove after refactoring implementation(projects.domain.legacy) - implementation(deps.kotlin.coroutines) - implementation(deps.arrow.core) + /** Tangem SDKs */ + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + /** DI */ implementation(deps.hilt.core) kapt(deps.hilt.kapt) + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + implementation(deps.moshi.kotlin) + implementation(deps.jodatime) implementation(deps.timber) - - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 85b6f06d93..2b512574c4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -1,11 +1,16 @@ package com.tangem.data.tokens.di +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.repository.DefaultTokensRepository import com.tangem.data.tokens.repository.MockNetworksRepository import com.tangem.data.tokens.repository.MockQuotesRepository -import com.tangem.data.tokens.repository.MockTokensRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +23,15 @@ internal object TokensDataModule { @Provides @Singleton - fun provideTokensRepository(): TokensRepository = MockTokensRepository() + fun provideTokensRepository( + tangemTechApi: TangemTechApi, + userTokensStore: UserTokensStore, + userWalletsStore: UserWalletsStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): TokensRepository { + return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + } @Provides @Singleton diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt index 3cf3584e68..ac1c0809ea 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -116,27 +116,14 @@ internal object MockTokens { derivationPath = null, ) - val tokens get() = mapOf( - UserWalletId(stringValue = "123") to setOf( - token1, token2, token3, token4, token5, - token6, token7, token8, token9, token10, - ), - UserWalletId(stringValue = "321") to setOf(token1, token2, token3), - UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), - UserWalletId(stringValue = "24") to setOf(token4, token5, token6), - ) - - val isGrouped get() = mapOf( - UserWalletId(stringValue = "123") to true, - UserWalletId(stringValue = "321") to false, - UserWalletId(stringValue = "42") to false, - UserWalletId(stringValue = "24") to true, - ) - - val isSortedByBalance get() = mapOf( - UserWalletId(stringValue = "123") to true, - UserWalletId(stringValue = "321") to false, - UserWalletId(stringValue = "42") to true, - UserWalletId(stringValue = "24") to false, - ) + val tokens + get() = mapOf( + UserWalletId(stringValue = "123") to setOf( + token1, token2, token3, token4, token5, + token6, token7, token8, token9, token10, + ), + UserWalletId(stringValue = "321") to setOf(token1, token2, token3), + UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), + UserWalletId(stringValue = "24") to setOf(token4, token5, token6), + ) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt new file mode 100644 index 0000000000..9d4a246327 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -0,0 +1,151 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.utils.CardTokensFactory +import com.tangem.data.tokens.utils.ResponseTokensFactory +import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class DefaultTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userTokensStore: UserTokensStore, + private val userWalletsStore: UserWalletsStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : TokensRepository { + + private val demoConfig = DemoConfig() + private val responseTokensFactory = ResponseTokensFactory(demoConfig) + private val cardTokensFactory = CardTokensFactory(demoConfig) + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun saveTokens( + userWalletId: UserWalletId, + tokens: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ) = withContext(dispatchers.io) { + val response = userTokensResponseFactory.createUserTokensResponse( + tokens = tokens, + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + storeAndPushTokens(userWalletId, response) + } + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> = channelFlow { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + + if (userWallet.isMultiCurrency) { + launch(dispatchers.io) { + getMultiCurrencyWalletTokens(userWallet).collectLatest(::send) + } + + launch(dispatchers.io) { + fetchTokensIfCacheExpired(userWallet, refresh) + } + } else { + send(getSingleCurrencyWalletTokens(userWallet)) + } + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.group == UserTokensResponse.GroupType.NETWORK } + .flowOn(dispatchers.io) + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.sort == UserTokensResponse.SortType.BALANCE } + .flowOn(dispatchers.io) + } + + private fun getMultiCurrencyWalletTokens(userWallet: UserWallet): Flow> { + return userTokensStore.get(userWallet.walletId).map { storedTokens -> + responseTokensFactory.createTokens( + response = storedTokens, + card = userWallet.scanResponse.card, + ) + } + } + + private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set { + var response = userTokensStore.getSyncOrNull(userWallet.walletId) + + if (response == null) { + response = userTokensResponseFactory.createUserTokensResponse( + tokens = cardTokensFactory.createTokensForSingleCurrencyCard(userWallet.scanResponse), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + userTokensStore.store(userWallet.walletId, response) + } + + return responseTokensFactory.createTokens(response, userWallet.scanResponse.card) + } + + private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { + cacheRegistry.invokeOnExpire( + key = getTokensCacheKey(userWallet.walletId), + skipCache = refresh, + block = { fetchTokens(userWallet) }, + ) + } + + private suspend fun fetchTokens(userWallet: UserWallet) { + try { + val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) + + userTokensStore.store(userWallet.walletId, response) + } catch (e: Throwable) { + handleFetchTokensErrorOrThrow(userWallet, e) + } + } + + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + userTokensStore.store(userWalletId, response) + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + } + + private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { + val errorMessage = error.message ?: throw error + + if (NOT_FOUND_HTTP_CODE in errorMessage) { + val response = userTokensStore.getSyncOrNull(userWallet.walletId) + ?: userTokensResponseFactory.createUserTokensResponse( + tokens = cardTokensFactory.createDefaultTokensForMultiCurrencyCard(userWallet.scanResponse.card), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) + } else { + throw error + } + } + + private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private companion object { + const val NOT_FOUND_HTTP_CODE = "404" + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt deleted file mode 100644 index 59e58db799..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.data.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.tokens.repository.TokensRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class MockTokensRepository : TokensRepository { - - override suspend fun saveTokens( - userWalletId: UserWalletId, - tokens: Set, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) = Unit - - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { - return flowOf(value = MockTokens.tokens[userWalletId]!!) - } - - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return flowOf(value = MockTokens.isGrouped[userWalletId]!!) - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return flowOf(value = MockTokens.isSortedByBalance[userWalletId]!!) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt index 671810e7e7..684a668b15 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -11,7 +11,6 @@ import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken import com.tangem.domain.tokens.model.Token as DomainToken -@Suppress("unused") // TODO: Will be used in next MR internal class CardTokensFactory(private val demoConfig: DemoConfig) { fun createDefaultTokensForMultiCurrencyCard(card: CardDTO): Set { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt index 462a115635..5ca7fea388 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt @@ -11,7 +11,6 @@ import com.tangem.domain.tokens.model.Token import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken -@Suppress("unused") // TODO: Will be used in next MR internal class ResponseTokensFactory(private val demoConfig: DemoConfig) { fun createTokens(response: UserTokensResponse, card: CardDTO): Set { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index 6c895bee65..721dc275f6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.Token -@Suppress("unused") // TODO: Will be used in next MR internal class UserTokensResponseFactory { fun createUserTokensResponse( From db4efb71a1ddabaf1037fd43b0246fd0409904eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 13:36:01 +0800 Subject: [PATCH 075/100] Updated on 2026-08-14 --- .../com/tangem/data/tokens/mock/MockTokens.kt | 230 +++++++++--------- features/wallet/impl/build.gradle.kts | 1 + .../router/DefaultWalletRouter.kt | 8 +- .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/WalletBottomSheetConfig.kt | 38 ++- .../wallet/state/WalletNotification.kt | 14 +- .../WalletLoadedTokensListConverter.kt | 53 ++++ .../WalletSkeletonStateConverter.kt} | 86 +++---- .../state/factory/WalletStateFactory.kt | 69 ++++++ .../wallet/ui/components/WalletBottomSheet.kt | 13 +- .../utils/TokenListToWalletStateConverter.kt | 70 ++++-- .../wallet/viewmodels/JobHolder.kt | 21 ++ .../wallet/viewmodels/WalletClickCallbacks.kt | 28 +++ .../WalletNotificationsListFactory.kt | 81 ++++-- .../wallet/viewmodels/WalletViewModel.kt | 194 +++++++++------ 15 files changed, 595 insertions(+), 314 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/{builder/WalletStateFactory.kt => factory/WalletSkeletonStateConverter.kt} (51%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt index ac1c0809ea..97384de272 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -5,116 +5,126 @@ import com.tangem.domain.wallets.models.UserWalletId internal object MockTokens { - val token1 = Token( - id = Token.ID("token1"), - networkId = MockNetworks.network1.id, - name = "Token 1", - symbol = "T1", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token2 = Token( - id = Token.ID("token2"), - networkId = MockNetworks.network1.id, - name = "Token 2", - symbol = "T2", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token3 = Token( - id = Token.ID("token3"), - networkId = MockNetworks.network1.id, - name = "Token 3", - symbol = "T3", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token4 = Token( - id = Token.ID("token4"), - networkId = MockNetworks.network2.id, - name = "Token 4", - symbol = "T4", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token5 = Token( - id = Token.ID("token5"), - networkId = MockNetworks.network2.id, - name = "Token 5", - symbol = "T5", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token6 = Token( - id = Token.ID("token6"), - networkId = MockNetworks.network2.id, - name = "Token 6", - symbol = "T6", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token7 = Token( - id = Token.ID("token7"), - networkId = MockNetworks.network3.id, - name = "Token 7", - symbol = "T7", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token8 = Token( - id = Token.ID("token8"), - networkId = MockNetworks.network3.id, - name = "Token 8", - symbol = "T8", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token9 = Token( - id = Token.ID("token9"), - networkId = MockNetworks.network3.id, - name = "Token 9", - symbol = "T9", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token10 = Token( - id = Token.ID("token10"), - networkId = MockNetworks.network3.id, - name = "Token 10", - symbol = "T10", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) + val token1 + get() = Token( + id = Token.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = null, + derivationPath = null, + ) + val token2 + get() = Token( + id = Token.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token3 + get() = Token( + id = Token.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token4 + get() = Token( + id = Token.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = null, + derivationPath = null, + ) + val token5 + get() = Token( + id = Token.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token6 + get() = Token( + id = Token.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token7 + get() = Token( + id = Token.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = null, + derivationPath = null, + ) + val token8 + get() = Token( + id = Token.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token9 + get() = Token( + id = Token.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token10 + get() = Token( + id = Token.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) val tokens get() = mapOf( diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 13a57f56c7..60ca50d490 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { /** Domain modules */ implementation(projects.common) implementation(projects.domain.card) + implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.settings) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 3964582227..85fb11d43b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -74,11 +74,15 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation } override fun openOrganizeTokensScreen() { - navController.navigate(WalletScreens.ORGANIZE_TOKENS.name) + navController.navigate(route = WalletScreens.ORGANIZE_TOKENS.name) } override fun openDetailsScreen() { - navigationStateHolder.navigate(NavigationAction.NavigateTo(AppScreen.Details)) + navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) + } + + override fun openOnboardingScreen() { + navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 520d008d6d..866bbc9c3d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -33,4 +33,7 @@ internal interface InnerWalletRouter : WalletRouter { /** Open details screen */ fun openDetailsScreen() + + /** Open onboarding screen */ + fun openOnboardingScreen() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt index 5aafcd4464..5a8f12209b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt @@ -29,10 +29,14 @@ internal data class WalletBottomSheetConfig( @DrawableRes open val iconResId: Int, open val tint: Color? = null, val primaryButtonConfig: ButtonConfig, - val secondaryButtonConfig: ButtonConfig? = null, + val secondaryButtonConfig: ButtonConfig, ) { - data class ButtonConfig(val text: String, val onClick: () -> Unit, @DrawableRes val iconResId: Int? = null) + data class ButtonConfig( + val text: TextReference, + val onClick: () -> Unit, + @DrawableRes val iconResId: Int? = null, + ) data class UnlockWallets( val onUnlockClick: () -> Unit, @@ -45,9 +49,9 @@ internal data class WalletBottomSheetConfig( ), iconResId = R.drawable.ic_locked_24, tint = TangemColorPalette.Black, - primaryButtonConfig = ButtonConfig(text = "Unlock", onClick = onUnlockClick), + primaryButtonConfig = ButtonConfig(text = TextReference.Str(value = "Unlock"), onClick = onUnlockClick), secondaryButtonConfig = ButtonConfig( - text = "Scan card", + text = TextReference.Str(value = "Scan card"), onClick = onScanClick, iconResId = R.drawable.ic_tangem_24, ), @@ -61,19 +65,35 @@ internal data class WalletBottomSheetConfig( subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"), iconResId = R.drawable.ic_star_24, tint = TangemColorPalette.Tangerine, - primaryButtonConfig = ButtonConfig(text = "Rate the app", onClick = onRateTheAppClick), - secondaryButtonConfig = ButtonConfig(text = "Share feedback", onClick = onShareClick), + primaryButtonConfig = ButtonConfig( + text = TextReference.Str(value = "Rate the app"), + onClick = onRateTheAppClick, + ), + secondaryButtonConfig = ButtonConfig( + text = TextReference.Str(value = "Share feedback"), + onClick = onShareClick, + ), ) - data class MultiWalletAlreadySignedHashes(val onLearnClick: () -> Unit) : BottomSheetContentConfig( + data class CriticalWarningAlreadySignedHashes( + val onOkClick: () -> Unit, + val onCancelClick: () -> Unit, + ) : BottomSheetContentConfig( title = TextReference.Res( id = R.string.warning_important_security_info, formatArgs = WrappedList(listOf("\u26A0")), ), - subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), + subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message), iconResId = R.drawable.img_attention_20, tint = null, - primaryButtonConfig = ButtonConfig(text = "Learn more", onClick = onLearnClick), + primaryButtonConfig = ButtonConfig( + text = TextReference.Res(id = R.string.common_ok), + onClick = onOkClick, + ), + secondaryButtonConfig = ButtonConfig( + text = TextReference.Res(id = R.string.common_cancel), + onClick = onCancelClick, + ), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt index b1813c0544..842f92ccfe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt @@ -81,26 +81,26 @@ sealed class WalletNotification(open val state: NotificationState) { ) /** - * "Wallet already signed hashes" notification + * "Already topped up and signed hashes" warning notification * - * @property onClick lambda be invoked when notification is clicked + * @property onClick lambda be invoked when notification's close button is clicked */ - data class AlreadyToppedUpAndSignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + data class WarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Closable( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), iconResId = R.drawable.img_attention_20, - onClick = onClick, tint = null, + onCloseClick = onClick, ), ) /** - * "Multi wallet already signed hashes" notification + * "Already signed hashes" critical warning notification * * @property onClick lambda be invoked when notification is clicked */ - data class SignedTransactionsInThePast(override val onClick: () -> Unit) : Clickable, WalletNotification( + data class CriticalWarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( state = NotificationState.Clickable( title = TextReference.Res( id = R.string.warning_important_security_info, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt new file mode 100644 index 0000000000..138f36b6fd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter +import com.tangem.utils.converter.Converter + +/** + * Converter from loaded [TokenListError] or [TokenList] to [WalletStateHolder] + * + * @property currentStateProvider current ui state provider + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadedTokensListConverter( + private val currentStateProvider: Provider, +) : Converter { + + private val tokenListStateConverter = TokenListToWalletStateConverter( + currentState = currentStateProvider(), + isWalletContentHidden = false, // TODO: [REDACTED_JIRA] + fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] + fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + ) + + private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter( + currentState = currentStateProvider(), + ) + + override fun convert(value: LoadedTokensListModel): WalletStateHolder { + return value.tokenListEither.fold( + ifLeft = tokenListErrorStateConverter::convert, + ifRight = { + tokenListStateConverter.convert( + value = TokenListToWalletStateConverter.TokensListModel( + tokenList = it, + isRefreshing = value.isRefreshing, + ), + ) + }, + ) + } + + data class LoadedTokensListModel( + val tokenListEither: Either, + val isRefreshing: Boolean, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt similarity index 51% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 081985c3f6..e372cb1500 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -1,85 +1,75 @@ -package com.tangem.feature.wallet.presentation.wallet.state.builder +package com.tangem.feature.wallet.presentation.wallet.state.factory -import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -internal class WalletStateFactory( - private val routerProvider: Provider, - private val onScanCardClick: () -> Unit, - private val onWalletChange: (Int) -> Unit, - private val onRefreshSwipe: () -> Unit, -) { +/** + * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder] + * + * @property clickCallbacks screen click callbacks + * +[REDACTED_AUTHOR] + */ +internal class WalletSkeletonStateConverter( + private val clickCallbacks: WalletClickCallbacks, +) : Converter, WalletStateHolder> { - fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick) - - fun getContentState(wallets: List): WalletStateHolder { - val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver + override fun convert(value: List): WalletStateHolder { + val cardTypeResolver = requireNotNull(value.firstOrNull()).scanResponse.cardTypesResolver return if (cardTypeResolver.isMultiwalletAllowed()) { - createMultiCurrencyState(wallets) + createMultiCurrencyState(value) } else { - createSingleCurrencyState(wallets) + createSingleCurrencyState(value, cardTypeResolver) } } private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { return WalletStateHolder.MultiCurrencyContent( - onBackClick = ::onBackClick, + onBackClick = clickCallbacks::onBackClick, topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), contentItems = persistentListOf(), notifications = persistentListOf(), - bottomSheet = WalletBottomSheetConfig( - // TODO: check notifications - isShow = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( - onRateTheAppClick = {}, - onShareClick = {}, - ), - ), - onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen, + bottomSheet = null, + onOrganizeTokensClick = clickCallbacks::onOrganizeTokensClick, ) } - private fun createSingleCurrencyState(wallets: List): WalletStateHolder.SingleCurrencyContent { + private fun createSingleCurrencyState( + wallets: List, + cardTypeResolver: CardTypesResolver, + ): WalletStateHolder.SingleCurrencyContent { return WalletStateHolder.SingleCurrencyContent( - onBackClick = ::onBackClick, + onBackClick = clickCallbacks::onBackClick, topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), contentItems = persistentListOf(), notifications = persistentListOf(), - bottomSheet = WalletBottomSheetConfig( - // TODO: check notifications - isShow = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( - onRateTheAppClick = {}, - onShareClick = {}, - ), - ), + bottomSheet = null, buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons - // TODO: create market price block - marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState, + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = cardTypeResolver.getBlockchain().currency, + ), ) } - private fun onBackClick() = routerProvider().popBackStack() - private fun createTopBarConfig(): WalletTopBarConfig { return WalletTopBarConfig( - onScanCardClick = onScanCardClick, - onMoreClick = routerProvider()::openDetailsScreen, + onScanCardClick = clickCallbacks::onScanCardClick, + onMoreClick = clickCallbacks::onDetailsClick, ) } @@ -88,22 +78,22 @@ internal class WalletStateFactory( selectedWalletIndex = 0, wallets = wallets.map { wallet -> WalletCardState.Loading( - id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO + // TODO remove after adding release logic for GetTokenListUseCase + id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { UserWalletId("123") } else { UserWalletId("321") }, - // TODO: wallet.walletId, title = wallet.name, - additionalInfo = "", // TODO + additionalInfo = "", // TODO add additional info resolver imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver), ) }.toImmutableList(), - onWalletChange = onWalletChange, + onWalletChange = clickCallbacks::onWalletChange, ) } private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe) + return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickCallbacks::onRefreshSwipe) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt new file mode 100644 index 0000000000..3aec8d0359 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -0,0 +1,69 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import kotlinx.collections.immutable.ImmutableList + +/** + * Main factory for creating [WalletStateHolder] + * + * @property currentStateProvider current ui state provider + * @property clickCallbacks screen click callbacks + */ +internal class WalletStateFactory( + private val currentStateProvider: Provider, + private val clickCallbacks: WalletClickCallbacks, +) { + + private val skeletonConverter = WalletSkeletonStateConverter(clickCallbacks = clickCallbacks) + private val loadedTokensListConverter = WalletLoadedTokensListConverter(currentStateProvider = currentStateProvider) + + fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickCallbacks::onBackClick) + + fun getSkeletonState(wallets: List): WalletStateHolder = skeletonConverter.convert(wallets) + + fun getStateByTokensList( + tokenListEither: Either, + isRefreshing: Boolean, + ): WalletStateHolder { + return loadedTokensListConverter.convert( + value = LoadedTokensListModel(tokenListEither = tokenListEither, isRefreshing = isRefreshing), + ) + } + + fun getStateByNotifications(notifications: ImmutableList): WalletStateHolder { + return currentStateProvider().copySealed(notifications = notifications) + } + + fun getStateAfterWalletChanging(index: Int): WalletStateHolder { + return currentStateProvider().let { stateHolder -> + stateHolder.copySealed(walletsListConfig = stateHolder.walletsListConfig.copy(selectedWalletIndex = index)) + } + } + + fun getStateAfterContentRefreshing(): WalletStateHolder { + return currentStateProvider().let { state -> + state.copySealed(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = true)) + } + } + + fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletStateHolder { + return currentStateProvider().let { state -> + state.copySealed( + bottomSheet = WalletBottomSheetConfig( + isShow = true, + onDismissRequest = { state.copySealed(bottomSheet = state.bottomSheet?.copy(isShow = false)) }, + content = content, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt index 45b7a7a752..fb7a83e1bf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt @@ -81,11 +81,10 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetConten Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { val buttonModifier = Modifier.fillMaxWidth() + PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) - if (config.secondaryButtonConfig != null) { - SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) - } + SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) } } } @@ -97,13 +96,13 @@ private fun PrimaryButton( ) { if (config.iconResId == null) { PrimaryButton( - text = config.text, + text = config.text.resolveReference(), onClick = config.onClick, modifier = modifier, ) } else { PrimaryButtonIconStart( - text = config.text, + text = config.text.resolveReference(), iconResId = config.iconResId, onClick = config.onClick, modifier = modifier, @@ -118,13 +117,13 @@ private fun SecondaryButton( ) { if (config.iconResId == null) { SecondaryButton( - text = config.text, + text = config.text.resolveReference(), onClick = config.onClick, modifier = modifier, ) } else { SecondaryButtonIconStart( - text = config.text, + text = config.text.resolveReference(), iconResId = config.iconResId, onClick = config.onClick, modifier = modifier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 1db861142b..b1374bbead 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -1,10 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.utils +import com.tangem.core.ui.components.transactions.TransactionState import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.SingleCurrencyContent import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -13,47 +16,62 @@ internal class TokenListToWalletStateConverter( private val isWalletContentHidden: Boolean, private val fiatCurrencyCode: String, private val fiatCurrencySymbol: String, -) : Converter { +) : Converter { - override fun convert(value: TokenList): WalletStateHolder { - return when (currentState) { - is MultiCurrencyContent -> currentState.updateWithTokenList(value) - is SingleCurrencyContent -> currentState.updateWithTokenList(value) - is WalletStateHolder.Loading, - is WalletStateHolder.UnlockWalletContent, - -> currentState + private val tokenListToContentConverter = TokenListToContentItemsConverter( + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + ) + + override fun convert(value: TokensListModel): WalletStateHolder { + val state = if (currentState is MultiCurrencyContent) { + currentState.updateWithTokenList(tokenList = value.tokenList) + } else { + currentState } + + return state.copySealed( + walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance), + pullToRefreshConfig = if (value.isRefreshing) { + state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) + } else { + state.pullToRefreshConfig + }, + ) } private fun MultiCurrencyContent.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { - val converter = TokenListToContentItemsConverter(isWalletContentHidden, fiatCurrencyCode, fiatCurrencySymbol) - - return this.copy( - walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), - contentItems = converter.convert(tokenList), - ) - } - - private fun SingleCurrencyContent.updateWithTokenList(tokenList: TokenList): SingleCurrencyContent { - return this.copy( - walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), - ) + return this.copy(contentItems = tokenListToContentConverter.convert(value = tokenList)) } private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { val selectedWalletIndex = walletsListConfig.selectedWalletIndex val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] val converter = FiatBalanceToWalletCardConverter( - selectedWalletCard, - isWalletContentHidden, - fiatCurrencyCode, - fiatCurrencySymbol, + currentState = selectedWalletCard, + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, ) return walletsListConfig.copy( wallets = walletsListConfig.wallets .toPersistentList() - .set(selectedWalletIndex, converter.convert(fiatBalance)), + .set(index = selectedWalletIndex, element = converter.convert(fiatBalance)), ) } + + private fun WalletStateHolder.getRefreshingStatus(): Boolean { + return contentItems.any { state -> + val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token + val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction + + isMultiCurrencyItem?.state is TokenItemState.Loading || + isSingleCurrencyItem?.state is TransactionState.Loading || + state is WalletContentItemState.Loading + } + } + + data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt new file mode 100644 index 0000000000..94c0e30ade --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import kotlinx.coroutines.Job + +/** + * Job holder. It is automatically finished old job if new one is started + * +[REDACTED_AUTHOR] + */ +internal class JobHolder { + + private var job: Job? = null + + /** Update current job */ + fun update(job: Job) { + this.job?.cancel() + this.job = job + } +} + +internal fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt new file mode 100644 index 0000000000..f47c10bc5c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +internal interface WalletClickCallbacks { + + fun onBackClick() + + fun onScanCardClick() + + fun onDetailsClick() + + fun onBackupCardClick() + + fun onCriticalWarningAlreadySignedHashesClick() + + fun onCloseWarningAlreadySignedHashesClick() + + fun onLikeTangemAppClick() + + fun onRateTheAppClick() + + fun onShareClick() + + fun onWalletChange(index: Int) + + fun onRefreshSwipe() + + fun onOrganizeTokensClick() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index f2de50be61..d0df6d7f3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -1,27 +1,38 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.common.Provider import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow /** + * Wallet notifications list factory + * + * @property wasCardScannedCallback callback that check if card was scanned + * @property isUserAlreadyRateAppCallback callback that check if card is user already rate app + * @property isDemoCardCallback callback that check if card is demo + * @property clickCallbacks screen click callbacks + * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") internal class WalletNotificationsListFactory( + private val currentStateProvider: Provider, private val wasCardScannedCallback: suspend (String) -> Boolean, private val isUserAlreadyRateAppCallback: suspend () -> Boolean, - private val onWalletAlreadySignedHashesClick: () -> Unit, - private val onMultiWalletAlreadySignedHashesClick: () -> Unit, - private val onBackupCardClick: () -> Unit, - private val hasUnreachableNetworksCallback: () -> Boolean, - private val onLikeTangemAppClick: () -> Unit, + private val isDemoCardCallback: (String) -> Boolean, + private val clickCallbacks: WalletClickCallbacks, ) { - fun create(cardTypesResolver: CardTypesResolver): Flow> { + fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList): Flow> { // TODO: [REDACTED_JIRA] order return flow { emit( @@ -33,31 +44,31 @@ internal class WalletNotificationsListFactory( addRemainingSignaturesLeftNotifications(cardTypesResolver) + val isDemo = isDemoCardCallback(cardTypesResolver.getCardId()) if (!cardTypesResolver.isReleaseFirmwareType()) { add(element = WalletNotification.DevCard) } else { - addReleaseSpecialNotifications(cardTypesResolver) + addReleaseSpecialNotifications(cardTypesResolver = cardTypesResolver, isDemo = isDemo) } - // TODO: [REDACTED_JIRA] - DemoModeDatasource + if (isDemo) { + add(element = WalletNotification.DemoCard) + } - if (hasUnreachableNetworksCallback()) { + if (hasUnreachableNetworks()) { add(element = WalletNotification.UnreachableNetworks) } if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) { - add(element = WalletNotification.BackupCard(onClick = onBackupCardClick)) + add(element = WalletNotification.BackupCard(onClick = clickCallbacks::onBackupCardClick)) } - /* TODO: [REDACTED_JIRA] - * List - * .filter { store -> - * store.walletsData.any { it.status is WalletDataModel.MissedDerivation } - * } - */ + if (tokenList.hasMissedDerivations()) { + add(element = WalletNotification.ScanCard(onClick = clickCallbacks::onScanCardClick)) + } if (isUserAlreadyRateAppCallback()) { - add(element = WalletNotification.LikeTangemApp(onClick = onLikeTangemAppClick)) + add(element = WalletNotification.LikeTangemApp(onClick = clickCallbacks::onLikeTangemAppClick)) } }.toImmutableList(), ) @@ -75,19 +86,21 @@ internal class WalletNotificationsListFactory( private suspend fun MutableList.addReleaseSpecialNotifications( cardTypesResolver: CardTypesResolver, + isDemo: Boolean, ) { - // TODO: [REDACTED_JIRA] !isDemo - if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed()) { + if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed() && + !isDemo + ) { if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) { add( - element = WalletNotification.SignedTransactionsInThePast( - onClick = onMultiWalletAlreadySignedHashesClick, + element = WalletNotification.CriticalWarningAlreadySignedHashes( + onClick = clickCallbacks::onCriticalWarningAlreadySignedHashesClick, ), ) } else if (cardTypesResolver.hasWalletSignedHashes()) { add( - element = WalletNotification.AlreadyToppedUpAndSignedHashes( - onClick = onWalletAlreadySignedHashesClick, + element = WalletNotification.WarningAlreadySignedHashes( + onClick = clickCallbacks::onCloseWarningAlreadySignedHashesClick, ), ) } @@ -98,6 +111,26 @@ internal class WalletNotificationsListFactory( } } + private fun hasUnreachableNetworks(): Boolean { + val isUnreachableState = { item: WalletContentItemState.MultiCurrencyItem -> + (item as? WalletContentItemState.MultiCurrencyItem.Token)?.state is TokenItemState.Unreachable + } + + return currentStateProvider().let { state -> + state is WalletStateHolder.MultiCurrencyContent && state.contentItems.any(isUnreachableState) + } + } + + private fun TokenList.hasMissedDerivations(): Boolean { + val statuses = when (this) { + is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::tokens).map(TokenStatus::value) + is TokenList.Ungrouped -> tokens.map(TokenStatus::value) + TokenList.NotInitialized -> emptyList() + } + + return statuses.any { it is TokenStatus.MissedDerivation } + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a369895e5b..13b9716f84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -4,30 +4,25 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* -import arrow.core.Either import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase -import com.tangem.domain.card.GetBiometricsStatusUseCase -import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.card.* +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.* -import com.tangem.feature.wallet.presentation.wallet.state.builder.WalletStateFactory -import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter -import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,74 +36,106 @@ import kotlin.properties.Delegates @Suppress("LongParameterList") @HiltViewModel internal class WalletViewModel @Inject constructor( + private val getWalletsUseCase: GetWalletsUseCase, private val saveWalletUseCase: SaveWalletUseCase, private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, - private val getWalletsUseCase: GetWalletsUseCase, + private val getCardWasScannedUseCase: GetCardWasScannedUseCase, + private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardProcessor: ScanCardProcessor, private val dispatchers: CoroutineDispatcherProvider, -) : ViewModel(), DefaultLifecycleObserver { +) : ViewModel(), DefaultLifecycleObserver, WalletClickCallbacks { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() + private val notificationsListFactory = WalletNotificationsListFactory( + currentStateProvider = Provider { uiState }, + wasCardScannedCallback = getCardWasScannedUseCase::invoke, + isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke, + isDemoCardCallback = isDemoCardUseCase::invoke, + clickCallbacks = this, + ) + private val stateFactory = WalletStateFactory( - routerProvider = Provider { router }, - onScanCardClick = ::onScanCardClick, - onWalletChange = ::changeWallet, - onRefreshSwipe = ::refreshContent, + currentStateProvider = Provider { uiState }, + clickCallbacks = this, ) /** Screen state */ - var uiState by mutableStateOf(stateFactory.getInitialState()) + var uiState: WalletStateHolder by mutableStateOf(stateFactory.getInitialState()) private set + private var cardTypeResolverMap: Map by Delegates.notNull() + + private val getTokensJobHolder = JobHolder() + private val getNotificationJobHolder = JobHolder() + override fun onCreate(owner: LifecycleOwner) { getWalletsUseCase() - .distinctUntilChanged() .flowWithLifecycle(owner.lifecycle) + .distinctUntilChanged() .onEach { wallets -> if (wallets.isEmpty()) return@onEach - uiState = stateFactory.getContentState(wallets = wallets) - updateContentItems() + cardTypeResolverMap = wallets.associate { wallet -> + // TODO remove after adding release logic for GetTokenListUseCase + if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { + UserWalletId("123") + } else { + UserWalletId("321") + } to wallet.scanResponse.cardTypesResolver + } + + uiState = stateFactory.getSkeletonState(wallets = wallets) + + updateContentItems(index = 0) } .flowOn(dispatchers.io) .launchIn(viewModelScope) } - @OptIn(ExperimentalCoroutinesApi::class) - private fun updateContentItems() { - getTokenListUseCase( - userWalletId = uiState.walletsListConfig.wallets.get( - index = uiState.walletsListConfig.selectedWalletIndex, - ).id, + private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { + // TODO: use GetTransactionsUseCase + + getTokenListUseCase(userWalletId = uiState.walletsListConfig.wallets[index].id) + .distinctUntilChanged() + .onEach { tokenListEither -> + uiState = stateFactory.getStateByTokensList( + tokenListEither = tokenListEither, + isRefreshing = isRefreshing, + ) + + tokenListEither.onRight { updateNotifications(index = index, tokenList = it) } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(jobHolder = getTokensJobHolder) + } + + private fun updateNotifications(index: Int, tokenList: TokenList) { + notificationsListFactory.create( + cardTypesResolver = requireNotNull( + value = cardTypeResolverMap[uiState.walletsListConfig.wallets[index].id], + lazyMessage = { + "CardTypeResolverMap doesn't contain this id = ${uiState.walletsListConfig.wallets[index].id}" + }, + ), + tokenList = tokenList, ) .distinctUntilChanged() - .mapLatest(::updateStateWithTokenListOrError) - .onEach { - uiState = it.copySealed( - pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()), - ) - } + .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } .flowOn(dispatchers.io) .launchIn(viewModelScope) + .saveIn(jobHolder = getNotificationJobHolder) } - private fun getRefreshingStatus(): Boolean { - return uiState.contentItems.any { state -> - val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token - val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction + override fun onBackClick() = router.popBackStack() - isMultiCurrencyItem?.state is TokenItemState.Loading || - isSingleCurrencyItem?.state is TransactionState.Loading || - state is WalletContentItemState.Loading - } - } - - private fun onScanCardClick() { + override fun onScanCardClick() { val prevRequestPolicyStatus = getBiometricsStatusUseCase() // Update access the code policy according access code saving status @@ -138,47 +165,52 @@ internal class WalletViewModel @Inject constructor( } } - private fun changeWallet(index: Int) { + override fun onDetailsClick() = router.openDetailsScreen() + + override fun onBackupCardClick() = router.openOnboardingScreen() + + override fun onCriticalWarningAlreadySignedHashesClick() { + uiState = stateFactory.getStateWithOpenBottomSheet( + content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes( + onOkClick = {}, + onCancelClick = {}, + ), + ) + } + + override fun onCloseWarningAlreadySignedHashesClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onLikeTangemAppClick() { + uiState = stateFactory.getStateWithOpenBottomSheet( + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = ::onRateTheAppClick, + onShareClick = ::onShareClick, + ), + ) + } + + override fun onRateTheAppClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onShareClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onWalletChange(index: Int) { if (uiState.walletsListConfig.selectedWalletIndex == index) return - uiState = when (val state = uiState) { - is WalletStateHolder.MultiCurrencyContent -> state.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) - is WalletStateHolder.SingleCurrencyContent -> state.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) - is WalletStateHolder.UnlockWalletContent -> state.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) - is WalletStateHolder.Loading -> state - } + uiState = stateFactory.getStateAfterWalletChanging(index = index) - updateContentItems() + updateContentItems(index = index) } - private fun refreshContent() { - uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true)) - updateContentItems() + override fun onRefreshSwipe() { + uiState = stateFactory.getStateAfterContentRefreshing() + updateContentItems(index = uiState.walletsListConfig.selectedWalletIndex, isRefreshing = true) } - private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { - val updateStateWithError = { error: TokenListError -> - val converter = TokenListErrorToWalletStateConverter(uiState) - - converter.convert(error) - } - val updateState = { list: TokenList -> - val converter = TokenListToWalletStateConverter( - uiState, - isWalletContentHidden = false, // TODO: [REDACTED_JIRA] - fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] - fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] - ) - - converter.convert(list) - } - - return tokenList.fold(ifLeft = updateStateWithError, ifRight = updateState) - } + override fun onOrganizeTokensClick() = router.openOrganizeTokensScreen() } \ No newline at end of file From 8b64e2679a90af5f4fe3bc007c4390d1d2a67ce0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jul 2023 10:51:06 +0400 Subject: [PATCH 076/100] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 6 ++++++ .../common/analytics/events/AnalyticsParam.kt | 2 ++ .../events/BlockchainExceptionEvent.kt | 17 ++++++++++++++++ .../handlers/BlockchainExceptionHandler.kt | 20 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 00e2ae0a86..b4783eda19 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -10,6 +10,7 @@ import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics @@ -31,6 +32,7 @@ import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggle import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler @@ -157,6 +159,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var scanCardProcessor: ScanCardProcessor + @Inject + lateinit var blockchainExceptionHandler: BlockchainExceptionHandler + override fun onCreate() { super.onCreate() @@ -280,6 +285,7 @@ class TapApplication : Application(), ImageLoaderFactory { jsonConverter = MoshiConverter.sdkMoshiConverter, ) factory.build(Analytics, buildData) + ExceptionHandler.append(blockchainExceptionHandler) } private fun initFeedbackManager( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 47623e1f10..28744c2332 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -157,5 +157,7 @@ sealed class AnalyticsParam { const val ERROR_KEY = "Error Key" const val CREATION_TYPE = "Creation type" const val DAPP_NAME = "DApp Name" + const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" + const val BLOCKCHAIN_SELECTED_HOST = "selected_host" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt new file mode 100644 index 0000000000..385ab3e517 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.core.analytics.models.AnalyticsEvent + +class BlockchainExceptionEvent( + selectedHost: String, + exceptionHost: String, + error: String, +) : AnalyticsEvent( + category = "BlockchainSdk", + event = "Exception", + params = mapOf( + AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost, + AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost, + AnalyticsParam.ERROR_DESCRIPTION to error, + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt new file mode 100644 index 0000000000..b227a5f1f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.common.analytics.handlers + +import com.tangem.blockchain.common.ExceptionHandlerOutput +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent +import javax.inject.Inject + +class BlockchainExceptionHandler @Inject constructor( + private val analyticsHandler: AnalyticsEventHandler, +) : ExceptionHandlerOutput { + override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { + analyticsHandler.send( + BlockchainExceptionEvent( + selectedHost = nextHost, + exceptionHost = currentHost, + error = message, + ), + ) + } +} \ No newline at end of file From 2317703e3734bf337fa989c1193f7a3e10247db9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 17:08:24 +0300 Subject: [PATCH 077/100] Updated on 2026-08-14 --- .../common/analytics/topup/TopUpController.kt | 2 +- .../tap/common/extensions/Collections.kt | 25 ----- .../tap/common/redux/global/GlobalReducer.kt | 2 +- .../tap/data/RuntimeUserWalletsStore.kt | 4 +- .../warningMessage/WarningMessagesManager.kt | 2 +- ...tUserWalletsPublicInformationRepository.kt | 11 +-- .../DefaultWalletAmountsRepository.kt | 7 +- .../datasource/di/CacheKeysStoreModule.kt | 7 +- .../datasource/di/UserTokensStoreModule.kt | 8 +- .../di/WalletManagersStoreModule.kt | 23 +++++ .../datasource/files/AndroidFileReader.kt | 4 + .../com/tangem/datasource/files/FileReader.kt | 4 + .../datasource/local/cache/CacheKeysStore.kt | 8 +- .../local/cache/DefaultCacheKeysStore.kt | 18 ++++ .../local/cache/RuntimeCacheKeysStore.kt | 25 ----- .../local/datastore/FileDataStore.kt | 42 ++++++--- .../local/datastore/RuntimeDataStore.kt | 76 +++++++-------- .../local/datastore/core/DataStore.kt | 18 ++++ .../datastore/core/StringKeyDataStore.kt | 3 + .../core/StringKeyDataStoreDecorator.kt | 36 +++++++ .../local/token/DefaultUserTokensStore.kt | 15 +++ .../local/token/FileUserTokensStore.kt | 29 ------ .../datasource/local/token/UserTokensStore.kt | 6 +- .../local/userwallet/UserWalletsStore.kt | 2 +- .../DefaultWalletManagersStore.kt | 43 +++++++++ .../walletmanager/WalletManagersStore.kt | 18 ++++ .../com/tangem/utils/extensions/Collection.kt | 94 ++++++++++++++++++- .../data/common/cache/DefaultCacheRegistry.kt | 4 +- .../repository/DefaultTokensRepository.kt | 2 +- 29 files changed, 369 insertions(+), 169 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Collections.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt index ba23fa52dd..b23f4a5b54 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt @@ -14,7 +14,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.converters.TopUpEventConverter import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.extensions.copy import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel @@ -22,6 +21,7 @@ import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.scope +import com.tangem.utils.extensions.copy import kotlinx.coroutines.launch import java.math.BigDecimal diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt b/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt deleted file mode 100644 index ee2867d25c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.common.extensions - -fun MutableList.removeBy(predicate: (T) -> Boolean): Boolean { - val toRemove = this.filter(predicate) - this.removeAll(toRemove) - return toRemove.isNotEmpty() -} - -fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { - val toRemove = this.filter(predicate) - if (toRemove.isEmpty()) return false - - val indexes = toRemove.map { indexOf(it) } - this.removeAll(toRemove) - indexes.forEach { this.add(it, item) } - return true -} - -fun MutableList.replaceByOrAdd(item: T, predicate: (T) -> Boolean) { - if (!replaceBy(item, predicate)) add(item) -} - -fun MutableList.copy(): MutableList { - return this.map { it }.toMutableList() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 15b8d10c94..d26e36149c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -2,11 +2,11 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.tap.common.extensions.replaceBy import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @Suppress("LongMethod", "ComplexMethod") diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 9b2167ff12..d46dfadcad 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -12,10 +12,10 @@ internal class RuntimeUserWalletsStore( private val walletsStateHolder: WalletsStateHolder, ) : UserWalletsStore { - override suspend fun getOrNull(userWalletId: UserWalletId): UserWallet? { + override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { return walletsStateHolder.userWalletsListManager ?.userWallets ?.firstOrNull() - ?.singleOrNull { it.walletId == userWalletId } + ?.singleOrNull { it.walletId == key } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 9239ac0d3f..183ba76207 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.configurable.warningMessage import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.extensions.removeBy +import com.tangem.utils.extensions.removeBy import com.tangem.wallet.R import java.util.concurrent.CopyOnWriteArrayList diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index cadd73b606..4ab48a80a9 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -9,10 +9,10 @@ import com.tangem.common.flatMap import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.utils.publicInformation +import com.tangem.utils.extensions.plusOrReplace import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -29,12 +29,9 @@ internal class DefaultUserWalletsPublicInformationRepository( getAll() .flatMap { savedInformation -> val infoToSave = withContext(Dispatchers.Default) { - savedInformation.toMutableList() - .apply { - replaceByOrAdd(userWallet.publicInformation) { - userWallet.walletId == it.walletId - } - } + savedInformation.plusOrReplace(userWallet.publicInformation) { + userWallet.walletId == it.walletId + } } save(infoToSave) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index b89be4b97f..6fa441f106 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -16,7 +16,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.TestActions import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository @@ -32,6 +31,7 @@ import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.plusOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -428,11 +428,10 @@ internal class DefaultWalletAmountsRepository( private suspend fun updateWalletManagerInStorage(userWalletId: UserWalletId, walletManager: WalletManager) = withContext(Dispatchers.Default) { WalletManagerStorage.update { prevManagers -> - val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply { - replaceByOrAdd(walletManager) { + val newManagersForUserWallet = prevManagers[userWalletId].orEmpty() + .plusOrReplace(walletManager) { it.wallet.blockchain == walletManager.wallet.blockchain } - } prevManagers.apply { set(userWalletId, newManagersForUserWallet) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt index ae96179015..ee39ca9041 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt @@ -1,7 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.cache.CacheKeysStore -import com.tangem.datasource.local.cache.RuntimeCacheKeysStore +import com.tangem.datasource.local.cache.DefaultCacheKeysStore +import com.tangem.datasource.local.datastore.RuntimeDataStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,6 +16,8 @@ internal object CacheKeysStoreModule { @Provides @Singleton fun provideCacheKeysStore(): CacheKeysStore { - return RuntimeCacheKeysStore() + return DefaultCacheKeysStore( + dataStore = RuntimeDataStore(), + ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt index 73d19f644d..757c01cef5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.token.FileUserTokensStore +import com.tangem.datasource.local.datastore.FileDataStore +import com.tangem.datasource.local.token.DefaultUserTokensStore import com.tangem.datasource.local.token.UserTokensStore import dagger.Module import dagger.Provides @@ -17,6 +19,8 @@ internal object UserTokensStoreModule { @Provides @Singleton fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore { - return FileUserTokensStore(fileReader, moshi) + return DefaultUserTokensStore( + dataStore = FileDataStore(fileReader, moshi.adapter(UserTokensResponse::class.java)), + ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt new file mode 100644 index 0000000000..7d7beb051e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.walletmanager.DefaultWalletManagersStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object WalletManagersStoreModule { + + @Provides + @Singleton + fun provideWalletManagersStore(): WalletManagersStore { + return DefaultWalletManagersStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt index 01b45b3a15..5957268681 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt @@ -19,4 +19,8 @@ class AndroidFileReader @Inject constructor(@ApplicationContext private val cont stream.write(content.toByteArray(), 0, content.length) } } + + override fun removeFile(fileName: String) { + context.deleteFile(fileName) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt index bdeb127b9f..27b0f8a8e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt @@ -1,6 +1,10 @@ package com.tangem.datasource.files interface FileReader { + fun readFile(fileName: String): String + fun rewriteFile(content: String, fileName: String) + + fun removeFile(fileName: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt index 1283fdd97d..962c09661a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -4,11 +4,11 @@ import com.tangem.datasource.local.cache.model.CacheKey interface CacheKeysStore { - fun getOrNull(id: String): CacheKey? + suspend fun getSyncOrNull(key: String): CacheKey? - fun addOrReplace(key: CacheKey) + suspend fun store(key: CacheKey) - fun remove(id: String) + suspend fun remove(key: String) - fun clear() + suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt new file mode 100644 index 0000000000..a2788d6a87 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator + +internal class DefaultCacheKeysStore( + dataStore: StringKeyDataStore, +) : CacheKeysStore, StringKeyDataStoreDecorator(dataStore) { + + override fun provideStringKey(key: String): String { + return key + } + + override suspend fun store(key: CacheKey) { + store(key.id, key) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt deleted file mode 100644 index 61ec6ce85c..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.datasource.local.cache - -import com.tangem.datasource.local.cache.model.CacheKey -import com.tangem.datasource.local.datastore.RuntimeDataStore - -internal class RuntimeCacheKeysStore : CacheKeysStore { - - private val store = RuntimeDataStore(keyProvider = CacheKey::id) - - override fun getOrNull(id: String): CacheKey? { - return store.getSync { it.id == id }.singleOrNull() - } - - override fun addOrReplace(key: CacheKey) { - store.addOrReplace(key) - } - - override fun remove(id: String) { - store.remove { it.id == id } - } - - override fun clear() { - store.clear() - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index dc647aad1c..c69c6d5c64 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -2,47 +2,59 @@ package com.tangem.datasource.local.datastore import com.squareup.moshi.JsonAdapter import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.datastore.model.WriteTrigger import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import timber.log.Timber -internal class FileDataStore( - private val fileNameProvider: (key: String) -> String, +internal class FileDataStore( private val fileReader: FileReader, - private val adapter: JsonAdapter, -) { + private val adapter: JsonAdapter, +) : StringKeyDataStore { private val writeTrigger = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - fun get(key: String): Flow { + override fun get(key: String): Flow { return writeTrigger .onEmpty { emit(WriteTrigger) } - .map { getInternal(fileNameProvider(key)) } + .map { getInternal(key) } .filterNotNull() } - fun getSyncOrNull(key: String): Data? { - return getInternal(fileNameProvider(key)) + override suspend fun getSyncOrNull(key: String): Value? { + return getInternal(key) } - fun store(key: String, content: Data) { - val fileName = fileNameProvider(key) - + override suspend fun store(key: String, item: Value) { try { - val json = adapter.toJson(content) + val json = adapter.toJson(item) - fileReader.rewriteFile(json, fileName) + fileReader.rewriteFile(json, key) writeTrigger.tryEmit(WriteTrigger) } catch (e: Throwable) { - Timber.e(e, "Unable to write file: $fileName") + Timber.e(e, "Unable to write file: $key") } } - private fun getInternal(fileName: String): Data? { + override suspend fun store(items: Map) { + items.forEach { (key, item) -> + store(key, item) + } + } + + override suspend fun remove(key: String) { + fileReader.removeFile(key) + } + + override suspend fun clear() { + // TODO: Implement if needed + } + + private fun getInternal(fileName: String): Value? { return try { val json = fileReader.readFile(fileName) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index fced00f858..d1c4eff1eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -1,59 +1,49 @@ package com.tangem.datasource.local.datastore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import kotlinx.coroutines.flow.* -internal class RuntimeDataStore(private val keyProvider: (Data) -> Key) { +internal class RuntimeDataStore : StringKeyDataStore { - private val store = MutableStateFlow>(hashMapOf()) + private val store = MutableStateFlow>(hashMapOf()) - fun get(selector: (Data) -> Boolean = { true }): Flow> { - return store.map { getInternal(it, selector) } + override fun get(key: String): Flow { + return store + .map { value -> value[key] } + .filterNotNull() } - fun getSync(selector: (Data) -> Boolean = { true }): List { - return getInternal(store.value, selector) + override suspend fun getSyncOrNull(key: String): Data? { + return store.value[key] } - // TODO: Uncomment if needed - // fun addOrReplace(items: Collection) { - // if (items.isEmpty()) return - // - // val storeValue = store.value - // - // items.forEach { item -> - // storeValue[keyProvider(item)] = item - // } - // - // store.value = storeValue - // } + override suspend fun store(key: String, item: Data) { + store.update { value -> + value[key] = item - fun addOrReplace(item: Data) { - val storeValue = store.value - storeValue[keyProvider(item)] = item - - store.value = storeValue - } - - fun remove(selector: (Data) -> Boolean) { - val storeValue = store.value - - storeValue.forEach { (key, item) -> - if (selector(item)) { - storeValue.remove(key) - } + value } - - store.value = storeValue } - fun clear() { + override suspend fun store(items: Map) { + store.update { value -> + items.forEach { (key, item) -> + value[key] = item + } + + value + } + } + + override suspend fun remove(key: String) { + store.update { value -> + value.remove(key) + + value + } + } + + override suspend fun clear() { store.update { hashMapOf() } } - - private fun getInternal(store: HashMap, selector: (Data) -> Boolean): List { - return store.values.filter { selector(it) } - } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt new file mode 100644 index 0000000000..7374c56980 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.datastore.core + +import kotlinx.coroutines.flow.Flow + +internal interface DataStore { + + fun get(key: Key): Flow + + suspend fun getSyncOrNull(key: Key): Value? + + suspend fun store(key: Key, item: Value) + + suspend fun store(items: Map) + + suspend fun remove(key: Key) + + suspend fun clear() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt new file mode 100644 index 0000000000..4c04a89dc5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.local.datastore.core + +internal interface StringKeyDataStore : DataStore \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt new file mode 100644 index 0000000000..92daa1a731 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -0,0 +1,36 @@ +package com.tangem.datasource.local.datastore.core + +import kotlinx.coroutines.flow.Flow + +internal abstract class StringKeyDataStoreDecorator( + private val dataStore: StringKeyDataStore, +) : DataStore { + + abstract fun provideStringKey(key: Key): String + + override fun get(key: Key): Flow { + return dataStore.get(provideStringKey(key)) + } + + override suspend fun getSyncOrNull(key: Key): Value? { + return dataStore.getSyncOrNull(provideStringKey(key)) + } + + override suspend fun store(key: Key, item: Value) { + dataStore.store(provideStringKey(key), item) + } + + override suspend fun store(items: Map) { + dataStore.store( + items = items.mapKeys { (key, _) -> provideStringKey(key) }, + ) + } + + override suspend fun remove(key: Key) { + dataStore.remove(provideStringKey(key)) + } + + override suspend fun clear() { + dataStore.clear() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt new file mode 100644 index 0000000000..43e895bc89 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultUserTokensStore( + dataStore: StringKeyDataStore, +) : UserTokensStore, StringKeyDataStoreDecorator(dataStore) { + + override fun provideStringKey(key: UserWalletId): String { + return key.stringValue + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt deleted file mode 100644 index 3c6e41fd4a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.datasource.local.token - -import com.squareup.moshi.Moshi -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.datastore.FileDataStore -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserTokensStore { - - private val store = FileDataStore( - fileNameProvider = { key -> "user_tokens_$key" }, - fileReader = fileReader, - adapter = moshi.adapter(UserTokensResponse::class.java), - ) - - override fun get(userWalletId: UserWalletId): Flow { - return store.get(userWalletId.stringValue) - } - - override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { - return store.getSyncOrNull(userWalletId.stringValue) - } - - override suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) { - store.store(userWalletId.stringValue, tokens) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt index 8b64c93767..ed175a871a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.Flow interface UserTokensStore { - fun get(userWalletId: UserWalletId): Flow + fun get(key: UserWalletId): Flow - suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? + suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? - suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) + suspend fun store(key: UserWalletId, item: UserTokensResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 4b47934b9a..a42253ee50 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,5 +5,5 @@ import com.tangem.domain.wallets.models.UserWalletId interface UserWalletsStore { - suspend fun getOrNull(userWalletId: UserWalletId): UserWallet? + suspend fun getSyncOrNull(key: UserWalletId): UserWallet? } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt new file mode 100644 index 0000000000..bed4e6a55c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -0,0 +1,43 @@ +package com.tangem.datasource.local.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.extensions.plusOrReplace + +internal class DefaultWalletManagersStore( + dataStore: StringKeyDataStore>, +) : WalletManagersStore, StringKeyDataStoreDecorator>(dataStore) { + + override fun provideStringKey(key: UserWalletId): String { + return key.stringValue + } + + override suspend fun getSyncOrNull( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): WalletManager? { + val walletManagers = getSyncOrNull(userWalletId) + + return walletManagers?.singleOrNull { + it.wallet.blockchain == blockchain && + it.wallet.publicKey.derivationPath?.rawPath == derivationPath + } + } + + override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) { + val walletManagers = getSyncOrNull(userWalletId) + + val updatedWalletManagers = walletManagers + ?.plusOrReplace(walletManager) { + it.wallet.blockchain == walletManager.wallet.blockchain && + it.wallet.publicKey == walletManager.wallet.publicKey + } + ?: listOf(walletManager) + + store(userWalletId, updatedWalletManagers) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt new file mode 100644 index 0000000000..c025cf6911 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletManagersStore { + + suspend fun getSyncOrNull( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): WalletManager? + + suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) + + suspend fun clear() +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 89fd92851b..36ef7e1ad6 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -3,4 +3,96 @@ package com.tangem.utils.extensions /** [REDACTED_AUTHOR] */ -fun Collection.isSingleItem(): Boolean = this.size == 1 \ No newline at end of file + +/** + * Checks if the [Collection] contains a single item. + * + * @return [Boolean] indicating whether the [Collection] contains exactly one element. + */ +fun Collection.isSingleItem(): Boolean = this.size == 1 + +/** + * Creates a shallow copy of this [Collection]. + * + * @return A copy of the [Collection]. + */ +fun Collection.copy(): Collection { + return this.map { it } +} + +/** + * Adds the specified element to the collection or replaces an existing element. + * The predicate defines the condition to replace the existing element. + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + * @return The modified [List] after adding or replacing the element. + */ +inline fun Collection.plusOrReplace(item: T, predicate: (T) -> Boolean): List { + val mutableList = this as? MutableList ?: ArrayList(this) + + mutableList.addOrReplace(item, predicate) + + return mutableList +} + +/** + * Adds the specified element to the collection or replaces an existing element. + * The predicate defines the condition to replace the existing element. + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + */ +inline fun MutableCollection.addOrReplace(item: T, predicate: (T) -> Boolean) { + val isReplaced = replaceBy(item, predicate) + + if (!isReplaced) { + add(item) + } +} + +/** + * Removes an element from the collection based on the provided predicate. + * + * @param predicate The condition to remove an element. + * @return [Boolean] indicating whether an element was removed. + */ +inline fun MutableCollection.removeBy(predicate: (T) -> Boolean): Boolean { + var removed = false + val iterator = this.iterator() + + for (e in iterator) { + if (predicate(e)) { + iterator.remove() + removed = true + + break + } + } + + return removed +} + +/** + * Replaces an element in the collection with the provided item based on the predicate. + * + * @param item The element to replace the existing one. + * @param predicate The condition to replace an existing element. + * @return [Boolean] indicating whether an element was replaced. + */ +inline fun MutableCollection.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { + var replaced = false + val mutableList = this as? MutableList ?: ArrayList(this) + val iterator = mutableList.listIterator() + + for (e in iterator) { + if (predicate(e)) { + iterator.set(item) + replaced = true + + break + } + } + + return replaced +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 47bfbc3675..b8380debee 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -10,7 +10,7 @@ internal class DefaultCacheRegistry( ) : CacheRegistry { override suspend fun isExpired(key: String): Boolean { - val cacheKey = cacheKeysStore.getOrNull(key) ?: return true + val cacheKey = cacheKeysStore.getSyncOrNull(key) ?: return true return cacheKey.updatedAt .plus(cacheKey.expiresIn) @@ -34,7 +34,7 @@ internal class DefaultCacheRegistry( val isExpired = isExpired(key) || skipCache if (!isExpired) return - cacheKeysStore.addOrReplace( + cacheKeysStore.store( key = CacheKey( id = key, updatedAt = LocalDateTime.now(), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt index 9d4a246327..5fa21ece16 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -48,7 +48,7 @@ internal class DefaultTokensRepository( override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> = channelFlow { val userWallet = withContext(dispatchers.io) { - requireNotNull(userWalletsStore.getOrNull(userWalletId)) { + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" } } From f2ab3a948d0cf1359dbddcf49dbebed66ce61862 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Jul 2023 15:56:59 +0500 Subject: [PATCH 078/100] Updated on 2026-08-14 --- .../empty/EmptyTransactionBlock.kt | 92 +++++++++++++++++++ .../empty/EmptyTransactionsBlockState.kt | 37 ++++++++ .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../main/res/drawable/ic_alert_history_64.xml | 10 ++ .../src/main/res/drawable/ic_compass_64.xml | 28 ++++++ core/ui/src/main/res/drawable/img_coin_64.xml | 57 ++++++++++++ 6 files changed, 225 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt create mode 100644 core/ui/src/main/res/drawable/ic_alert_history_64.xml create mode 100644 core/ui/src/main/res/drawable/ic_compass_64.xml create mode 100644 core/ui/src/main/res/drawable/img_coin_64.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt new file mode 100644 index 0000000000..4bb5a8f0b1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -0,0 +1,92 @@ +package com.tangem.core.ui.components.transactions.empty + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.buttons.actions.ActionButton +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.R + +@Composable +fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing24), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size64), + painter = painterResource(id = state.iconRes), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing32), + textAlign = TextAlign.Center, + text = state.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + ActionButton(config = state.actionButtonConfig) + } +} + +@Preview +@Composable +private fun EmptyTransactionBlock_Light( + @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, +) { + TangemTheme { + EmptyTransactionBlock(state = state) + } +} + +@Preview +@Composable +private fun EmptyTransactionBlock_Dark( + @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, +) { + TangemTheme(isDark = true) { + EmptyTransactionBlock(state = state) + } +} + +private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + EmptyTransactionsBlockState.Empty( + actionButtonConfig = ActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ), + EmptyTransactionsBlockState.FailedToLoad( + actionButtonConfig = ActionButtonConfig( + text = "Reload", + iconResId = R.drawable.ic_refresh_24, + onClick = {}, + ), + ), + EmptyTransactionsBlockState.NotImplemented( + actionButtonConfig = ActionButtonConfig( + text = "Explore transaction history", + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = {}, + ), + ), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt new file mode 100644 index 0000000000..bb26676865 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt @@ -0,0 +1,37 @@ +package com.tangem.core.ui.components.transactions.empty + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference + +sealed class EmptyTransactionsBlockState { + + @get:DrawableRes + abstract val iconRes: Int + + abstract val text: TextReference + + abstract val actionButtonConfig: ActionButtonConfig + + data class FailedToLoad( + override val actionButtonConfig: ActionButtonConfig, + ) : EmptyTransactionsBlockState() { + override val iconRes: Int = R.drawable.ic_alert_history_64 + override val text: TextReference = TextReference.Res(R.string.transaction_history_error_failed_to_load) + } + + data class Empty( + override val actionButtonConfig: ActionButtonConfig, + ) : EmptyTransactionsBlockState() { + override val iconRes: Int = R.drawable.img_coin_64 + override val text: TextReference = TextReference.Res(R.string.transaction_history_empty_transactions) + } + + data class NotImplemented( + override val actionButtonConfig: ActionButtonConfig, + ) : EmptyTransactionsBlockState() { + override val iconRes: Int = R.drawable.ic_compass_64 + override val text: TextReference = TextReference.Res(R.string.transaction_history_not_supported_description) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 8e17a060f2..ddba0a20cd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -63,6 +63,7 @@ data class TangemDimens internal constructor( val size52: Dp = 52.dp, val size56: Dp = 56.dp, val size62: Dp = 62.dp, + val size64: Dp = 64.dp, val size68: Dp = 68.dp, val size70: Dp = 70.dp, val size72: Dp = 72.dp, diff --git a/core/ui/src/main/res/drawable/ic_alert_history_64.xml b/core/ui/src/main/res/drawable/ic_alert_history_64.xml new file mode 100644 index 0000000000..51e0bb6e57 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert_history_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_compass_64.xml b/core/ui/src/main/res/drawable/ic_compass_64.xml new file mode 100644 index 0000000000..0d10b40a2c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_compass_64.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_coin_64.xml b/core/ui/src/main/res/drawable/img_coin_64.xml new file mode 100644 index 0000000000..c036101c31 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_coin_64.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + From 171732a41a39712809541d57012bd0509e285ec9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 18:28:34 +0300 Subject: [PATCH 079/100] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 ++++ .../repository/DefaultTokensRepository.kt | 39 +++++++------ .../data/tokens/utils/CardTokensFactory.kt | 10 +--- .../com/tangem/domain/core/error/DataError.kt | 7 --- .../domain/tokens/GetTokenListUseCase.kt | 5 +- .../tangem/domain/tokens/GetTokenUseCase.kt | 57 +++++++++++++++++++ .../tangem/domain/tokens/error/TokenError.kt | 8 +++ .../mapper/GetWalletTokenErrorMappers.kt | 14 +++++ .../tangem/domain/tokens/model/TokenStatus.kt | 3 + .../operations/TokenStatusOperations.kt | 2 + .../operations/TokensStatusesOperations.kt | 39 +++++++++++-- .../tokens/repository/TokensRepository.kt | 4 +- .../domain/tokens/mock/MockTokensStates.kt | 1 + .../tokens/repository/MockTokensRepository.kt | 7 ++- 14 files changed, 162 insertions(+), 45 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index b593c7a762..ddf863762d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -26,6 +26,17 @@ internal object TokensDomainModule { return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetTokenUseCase( + tokensRepository: TokensRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetTokenUseCase { + return GetTokenUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt index 5fa21ece16..1c83a87d60 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -46,14 +46,31 @@ internal class DefaultTokensRepository( storeAndPushTokens(userWalletId, response) } - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> = channelFlow { + override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { val userWallet = withContext(dispatchers.io) { requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" } } + require(!userWallet.isMultiCurrency) { + "Single currency wallet excepted, but multi currency wallet was found: $userWalletId" + } + + return cardTokensFactory + .createPrimaryTokenForSingleCurrencyCard(userWallet.scanResponse) + } + + override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return channelFlow { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + require(!userWallet.isMultiCurrency) { + "Multi currency wallet excepted, but single currency wallet was found: $userWalletId" + } - if (userWallet.isMultiCurrency) { launch(dispatchers.io) { getMultiCurrencyWalletTokens(userWallet).collectLatest(::send) } @@ -61,8 +78,6 @@ internal class DefaultTokensRepository( launch(dispatchers.io) { fetchTokensIfCacheExpired(userWallet, refresh) } - } else { - send(getSingleCurrencyWalletTokens(userWallet)) } } @@ -87,22 +102,6 @@ internal class DefaultTokensRepository( } } - private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set { - var response = userTokensStore.getSyncOrNull(userWallet.walletId) - - if (response == null) { - response = userTokensResponseFactory.createUserTokensResponse( - tokens = cardTokensFactory.createTokensForSingleCurrencyCard(userWallet.scanResponse), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - userTokensStore.store(userWallet.walletId, response) - } - - return responseTokensFactory.createTokens(response, userWallet.scanResponse.card) - } - private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { cacheRegistry.invokeOnExpire( key = getTokensCacheKey(userWallet.walletId), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt index 684a668b15..d41f73f91b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -27,7 +27,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { return blockchains.mapNotNull { createCoin(it, card) }.toSet() } - fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set { + fun createPrimaryTokenForSingleCurrencyCard(scanResponse: ScanResponse): DomainToken { val card = scanResponse.card val resolver = scanResponse.cardTypesResolver val blockchain = resolver.getBlockchain() @@ -39,13 +39,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { createToken(token, blockchain, card) } - return buildSet { - add(coin) - - if (primaryToken != null) { - add(primaryToken) - } - } + return primaryToken ?: coin } private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt index a76794c4c4..11567ca43b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -6,11 +6,4 @@ sealed class DataError : Exception() { object NoInternetConnection : NetworkError() } - - sealed class PersistenceError : DataError() { - - data class UnableToReadFile(override val cause: Throwable) : DataError() - - data class UnableToWriteFile(override val cause: Throwable) : DataError() - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index fcd8578f39..9f07999ccd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -18,6 +18,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flatMapConcat class GetTokenListUseCase( @@ -31,7 +32,7 @@ class GetTokenListUseCase( return channelFlow { recover( block = { - getTokenList(userWalletId, refresh).collect { list -> + getTokenList(userWalletId, refresh).collectLatest { list -> send(list.right()) } }, @@ -59,7 +60,7 @@ class GetTokenListUseCase( transformError = TokensStatusesOperations.Error::mapToTokenListError, ) - return operations.getTokensStatusesFlow() + return operations.getMultiCurrencyWalletTokensStatusesFlow() } private fun Raise.createTokenList( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt new file mode 100644 index 0000000000..fd3ea69f0b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt @@ -0,0 +1,57 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.recover +import arrow.core.right +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.error.mapper.mapToTokenError +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.operations.TokensStatusesOperations +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest + +class GetTokenUseCase( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow> { + return channelFlow { + recover( + block = { + getToken(userWalletId, refresh).collectLatest { token -> + send(token.right()) + } + }, + recover = { error -> + send(error.left()) + }, + ) + } + } + + private suspend fun Raise.getToken(userWalletId: UserWalletId, refresh: Boolean): Flow { + val operations = TokensStatusesOperations( + tokensRepository = tokensRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + refresh = refresh, + dispatchers = dispatchers, + raise = this, + transformError = TokensStatusesOperations.Error::mapToTokenError, + ) + + return operations.getSingleCurrencyWalletTokenStatusFlow() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt new file mode 100644 index 0000000000..b8b3e0631b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.error + +sealed class TokenError { + + object UnableToCreateToken : TokenError() + + data class DataError(val cause: Throwable) : TokenError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt new file mode 100644 index 0000000000..25f2bf46d3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.operations.TokensStatusesOperations + +internal fun TokensStatusesOperations.Error.mapToTokenError(): TokenError { + return when (this) { + is TokensStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) + is TokensStatusesOperations.Error.EmptyNetworksStatuses, + is TokensStatusesOperations.Error.EmptyQuotes, + is TokensStatusesOperations.Error.EmptyTokens, + -> TokenError.UnableToCreateToken + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt index 1ea03d8aa3..9a217c2f35 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt @@ -16,6 +16,7 @@ data class TokenStatus( sealed class Status { open val amount: BigDecimal? = null open val fiatAmount: BigDecimal? = null + open val fiatRate: BigDecimal? = null open val priceChange: BigDecimal? = null open val hasTransactionsInProgress: Boolean = false } @@ -31,6 +32,7 @@ data class TokenStatus( data class Loaded( override val amount: BigDecimal, override val fiatAmount: BigDecimal, + override val fiatRate: BigDecimal, override val priceChange: BigDecimal, override val hasTransactionsInProgress: Boolean, ) : Status() @@ -38,6 +40,7 @@ data class TokenStatus( data class Custom( override val amount: BigDecimal, override val fiatAmount: BigDecimal?, + override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, override val hasTransactionsInProgress: Boolean, ) : Status() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt index dfb9d9f8bc..250ba9932f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -48,6 +48,7 @@ internal class TokenStatusOperations( token.isCustom -> TokenStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), + fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, hasTransactionsInProgress = hasTransactionsInProgress, ) @@ -55,6 +56,7 @@ internal class TokenStatusOperations( else -> TokenStatus.Loaded( amount = amount, fiatAmount = calculateFiatAmount(amount, quote.fiatRate), + fiatRate = quote.fiatRate, priceChange = quote.priceChange, hasTransactionsInProgress = hasTransactionsInProgress, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt index cbe9d303ad..7b7eced70a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens.operations -import arrow.core.NonEmptySet +import arrow.core.* import arrow.core.raise.Raise -import arrow.core.toNonEmptySetOrNull +import arrow.core.raise.catch import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.* @@ -43,8 +43,8 @@ internal class TokensStatusesOperations( transformError = transformError, ) - fun getTokensStatusesFlow(): Flow> { - return getTokens().flatMapConcat { + fun getMultiCurrencyWalletTokensStatusesFlow(): Flow> { + return getMultiCurrencyWalletTokens().flatMapConcat { val tokens = it.toNonEmptySetOrNull() if (tokens == null) { @@ -60,6 +60,24 @@ internal class TokensStatusesOperations( } } + suspend fun getSingleCurrencyWalletTokenStatusFlow(): Flow { + val token = getSingleCurrencyWalletToken() + + val quoteFlow = getQuotes(nonEmptySetOf(token.id)) + .map { quotes -> + quotes.singleOrNull { it.tokenId == token.id } + } + + val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token))) + .map { statuses -> + statuses.singleOrNull { it.networkId == token.networkId } + } + + return combine(quoteFlow, statusFlow) { quote, networkStatus -> + createStatus(token, quote, networkStatus) + } + } + private suspend fun createTokensStatuses( tokens: Set, quotes: Set, @@ -84,13 +102,22 @@ internal class TokensStatusesOperations( return tokenStatusOperations.createTokenStatus() } - private fun getTokens(): Flow> { - return tokensRepository.getTokens(userWalletId, refresh) + private fun getMultiCurrencyWalletTokens(): Flow> { + return tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh) .catch { raise(Error.DataError(it)) } .onEmpty { raise(Error.EmptyTokens) } .flowOn(dispatchers.io) } + private suspend fun getSingleCurrencyWalletToken(): Token { + return withContext(dispatchers.io) { + catch( + block = { tokensRepository.getSingleCurrencyWalletToken(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + private fun getQuotes(tokensIds: NonEmptySet): Flow> { return quotesRepository.getQuotes(tokensIds, refresh) .catch { raise(Error.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index 06e0b286b3..c019c0a34d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -16,7 +16,9 @@ interface TokensRepository { isSortedByBalance: Boolean, ) - fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> + suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token + + fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> fun isTokensGrouped(userWalletId: UserWalletId): Flow diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index b712bc465d..6652b1bd69 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -140,6 +140,7 @@ internal object MockTokensStates { value = TokenStatus.Loaded( amount = amount, fiatAmount = fiatAmount, + fiatRate = quote.fiatRate, priceChange = quote.priceChange, hasTransactionsInProgress = false, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index 02b76b06d5..bffa7f0ac5 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -37,7 +38,11 @@ internal class MockTokensRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { + return MockTokens.token1 + } + + override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { return tokens.map { it.getOrElse { e -> throw e } } } From 4420365cd11b344e3efdf9f6d186b6e5f792b308 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jul 2023 17:29:54 +0400 Subject: [PATCH 080/100] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 3 + .../common/analytics/events/WalletConnect.kt | 46 ++++++++++-- .../walletconnect/WalletConnectManager.kt | 4 - .../walletconnect/WalletConnectSdkHelper.kt | 6 -- .../data/WalletConnectRepositoryImpl.kt | 74 ++++++++++++++++--- .../domain/WalletConnectInteractor.kt | 35 +++++---- .../domain/WalletConnectRepository.kt | 11 ++- .../domain/models/RequestData.kt | 8 ++ .../domain/models/WalletConnectError.kt | 2 + .../domain/models/WalletConnectEvents.kt | 1 + .../walletconnect/WalletConnectMiddleware.kt | 2 +- 11 files changed, 146 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 28744c2332..e5e4c697a6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -157,6 +157,9 @@ sealed class AnalyticsParam { const val ERROR_KEY = "Error Key" const val CREATION_TYPE = "Creation type" const val DAPP_NAME = "DApp Name" + const val DAPP_URL = "DApp Url" + const val METHOD_NAME = "Method Name" + const val VALIDATION = "Validation" const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" const val BLOCKCHAIN_SELECTED_HOST = "selected_host" } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index 49c49b912a..13c04ec76f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics.events import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.extensions.filterNotNull /** [REDACTED_AUTHOR] @@ -12,16 +13,51 @@ sealed class WalletConnect( ) : AnalyticsEvent("Wallet Connect", event, params, error) { class ScreenOpened : WalletConnect(event = "WC Screen Opened") - class NewSessionEstablished(dAppName: String) : WalletConnect( + class NewSessionEstablished(dAppName: String, dAppUrl: String) : WalletConnect( event = "New Session Established", params = mapOf( AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, ), ) - class SessionDisconnected : WalletConnect("Session Disconnected") - class RequestSigned : WalletConnect("Request Signed") + class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect( + event = "Session Disconnected", + params = mapOf( + AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, + ), + ) - class SignError(error: Throwable) : WalletConnect("Sign", error = error) - class TransactionError(error: Throwable) : WalletConnect("Transaction", error = error) + class RequestHandled( + params: RequestHandledParams, + ) : WalletConnect( + event = "Request Handled", + params = params.toParamsMap(), + ) + + data class RequestHandledParams( + val dAppName: String, + val dAppUrl: String, + val methodName: String, + val blockchain: String, + val errorCode: String? = null, + ) { + fun toParamsMap(): Map { + val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param + return mapOf( + AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, + AnalyticsParam.METHOD_NAME to methodName, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.VALIDATION to validation, + if (errorCode != null) AnalyticsParam.ERROR_CODE to errorCode else null to null, + ).filterNotNull() + } + } + + enum class Validation(val param: String) { + SUCCESS("Success"), + FAIL("Fail"), + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index a86a86719a..a4bda602c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -3,11 +3,9 @@ package com.tangem.tap.domain.walletconnect import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard -import com.tangem.core.analytics.Analytics import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError @@ -227,7 +225,6 @@ class WalletConnectManager { } private fun onSessionClosed(session: WCSession) { - Analytics.send(WalletConnect.SessionDisconnected()) sessions.remove(session.topic) walletConnectRepository.removeSession(session) store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) @@ -395,7 +392,6 @@ class WalletConnectManager { ), ) } - Analytics.send(WalletConnect.NewSessionEstablished("")) } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 03f8cf7d40..f84ad537dd 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -12,7 +12,6 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.hexToBigDecimal import com.tangem.blockchain.extensions.isAscii import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString @@ -23,7 +22,6 @@ import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType -import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder @@ -175,7 +173,6 @@ class WalletConnectSdkHelper { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error as BlockchainSdkError) null } @@ -205,7 +202,6 @@ class WalletConnectSdkHelper { ).toHexString() } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) } Timber.e(result.error.customMessage) null } @@ -244,7 +240,6 @@ class WalletConnectSdkHelper { ) } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error.customMessage) null } @@ -338,7 +333,6 @@ class WalletConnectSdkHelper { ) } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) } Timber.e(result.error.customMessage) null } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt index 9890f9863f..3304faa535 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt @@ -2,9 +2,10 @@ package com.tangem.tap.domain.walletconnect2.data import android.app.Application import arrow.core.flatten -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.domain.WcRequest import com.tangem.tap.domain.walletconnect2.domain.models.* @@ -25,6 +26,7 @@ import javax.inject.Inject class WalletConnectRepositoryImpl @Inject constructor( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, + private val analyticsHandler: AnalyticsEventHandler, ) : WalletConnectRepository { private var sessionProposal: Wallet.Model.SessionProposal? = null @@ -37,6 +39,8 @@ class WalletConnectRepositoryImpl @Inject constructor( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions + private var currentSessions: List = emptyList() + /** * @param projectId Project ID at https://cloud.walletconnect.com/ */ @@ -127,8 +131,12 @@ class WalletConnectRepositoryImpl @Inject constructor( // we can send approval automatically, because in WC 2.0 the list of chains is approved when // initial connection is established sendRequest( - topic = sessionRequest.topic, - id = sessionRequest.request.id, + RequestData( + topic = sessionRequest.topic, + requestId = sessionRequest.request.id, + blockchain = sessionRequest.chainId.toString(), + method = WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code, + ), result = "", ) } @@ -142,6 +150,7 @@ class WalletConnectRepositoryImpl @Inject constructor( id = sessionRequest.request.id, metaUrl = sessionRequest.peerMetaData?.url ?: "", metaName = sessionRequest.peerMetaData?.name ?: "", + method = sessionRequest.request.method, ), ) } @@ -263,7 +272,12 @@ class WalletConnectRepositoryImpl @Inject constructor( params = sessionApproval, onSuccess = { Timber.d("Approved successfully: $it") - Analytics.send(WalletConnect.NewSessionEstablished(sessionProposal.name)) + analyticsHandler.send( + WalletConnect.NewSessionEstablished( + dAppName = sessionProposal.name, + dAppUrl = sessionProposal.url, + ), + ) }, onError = { Timber.d("Error while approving: $it") @@ -278,23 +292,56 @@ class WalletConnectRepositoryImpl @Inject constructor( ) } - override fun sendRequest(topic: String, id: Long, result: String) { + override fun sendRequest(requestData: RequestData, result: String) { + val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( + WalletConnect.RequestHandled( + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + ), + ), + ) Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( - sessionTopic = topic, + sessionTopic = requestData.topic, jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( - id = id, + id = requestData.requestId, result = result, ), ), onSuccess = {}, onError = { - Analytics.send(WalletConnect.TransactionError(it.throwable)) + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + errorCode = WalletConnectError.ValidationError.toString(), + ) }, ) } - override fun rejectRequest(topic: String, id: Long) { + override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { + val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( + WalletConnect.RequestHandled( + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + errorCode = error.toString(), + ), + ), + ) + cancelRequest(requestData.topic, requestData.requestId) + } + + override fun cancelRequest(topic: String, id: Long) { Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = topic, @@ -325,10 +372,16 @@ class WalletConnectRepositoryImpl @Inject constructor( } override fun disconnect(topic: String) { + val session = currentSessions.find { it.topic == topic } Web3Wallet.disconnectSession( params = Wallet.Params.SessionDisconnect(topic), onSuccess = { - Analytics.send(WalletConnect.SessionDisconnected()) + analyticsHandler.send( + WalletConnect.SessionDisconnected( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + ), + ) updateSessions() Timber.d("Disconnected successfully: $it") }, @@ -364,6 +417,7 @@ class WalletConnectRepositoryImpl @Inject constructor( ) } Timber.d("Available sessions: $availableSessions") + currentSessions = availableSessions _activeSessions.emit(availableSessions) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index e37d7c2949..9f5ce79361 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,7 +1,5 @@ package com.tangem.tap.domain.walletconnect2.domain -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* @@ -148,8 +146,8 @@ class WalletConnectInteractor( walletConnectRepository.disconnect(topic) } - fun rejectRequest(topic: String, id: Long) { - walletConnectRepository.rejectRequest(topic, id) + fun cancelRequest(topic: String, id: Long) { + walletConnectRepository.cancelRequest(topic, id) } private suspend fun handleRequest(sessionRequest: WalletConnectEvents.SessionRequest) { @@ -164,16 +162,23 @@ class WalletConnectInteractor( null } } + val networkId = sessionRequest.chainId?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "" + val requestData = RequestData( + topic = sessionRequest.topic, + requestId = sessionRequest.id, + blockchain = networkId, + method = sessionRequest.method, + ) + if (error != null) { - walletConnectRepository.rejectRequest(sessionRequest.topic, sessionRequest.id) + walletConnectRepository.rejectRequest(requestData, error) return } when (sessionRequest.request) { is WcRequest.BnbCancel -> Unit is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest( - topic = sessionRequest.topic, - id = sessionRequest.id, + requestData = requestData, result = "", ) else -> { @@ -212,16 +217,18 @@ class WalletConnectInteractor( Timber.d("Signed hash: $signedHash") + val requestData = RequestData( + topic = request.topic, + requestId = request.requestId, + blockchain = networkId, + method = currentRequest.method, + ) + if (signedHash == null) { - walletConnectRepository.rejectRequest( - topic = request.topic, - id = request.requestId, - ) + walletConnectRepository.rejectRequest(requestData, WalletConnectError.SigningError) } else { - Analytics.send(WalletConnect.RequestSigned()) walletConnectRepository.sendRequest( - topic = request.topic, - id = request.requestId, + requestData = requestData, result = signedHash, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt index 4069a56fa8..93e772b0d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt @@ -1,9 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain -import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.domain.models.NetworkNamespace -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectSession +import com.tangem.tap.domain.walletconnect2.domain.models.* import kotlinx.coroutines.flow.Flow interface WalletConnectRepository { @@ -24,7 +21,9 @@ interface WalletConnectRepository { fun reject() - fun sendRequest(topic: String, id: Long, result: String) + fun sendRequest(requestData: RequestData, result: String) - fun rejectRequest(topic: String, id: Long) + fun rejectRequest(requestData: RequestData, error: WalletConnectError) + + fun cancelRequest(topic: String, id: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt new file mode 100644 index 0000000000..38cd19ca97 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class RequestData( + val topic: String, + val requestId: Long, + val method: String, + val blockchain: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt index 35da4b88f8..ca01b701a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -7,4 +7,6 @@ sealed class WalletConnectError : Exception() { data class ExternalApprovalError(override val message: String?) : WalletConnectError() object WrongUserWallet : WalletConnectError() object UnsupportedMethod : WalletConnectError() + object SigningError : WalletConnectError() + object ValidationError : WalletConnectError() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt index e481daa22d..d47c296a42 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -24,5 +24,6 @@ sealed interface WalletConnectEvents { val id: Long, val metaName: String, val metaUrl: String, + val method: String, ) : WalletConnectEvents } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index b3b5057a76..c2276c9065 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -182,7 +182,7 @@ class WalletConnectMiddleware { } is WalletConnectAction.RejectRequest -> { walletConnectManager.rejectRequest(action.topic, action.id) - walletConnectInteractor.rejectRequest(action.topic, action.id) + walletConnectInteractor.cancelRequest(action.topic, action.id) } is WalletConnectAction.SendTransaction -> { walletConnectManager.completeTransaction(action.topic) From 3ff771e48b44dc335564affae626c82e16fec41f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 10:18:27 +0300 Subject: [PATCH 081/100] Updated on 2026-08-14 --- .../tokens/ApplyTokenListSortingUseCase.kt | 55 +++++++++++++-- .../ApplyTokenListSortingUseCaseTest.kt | 68 ++++++++++++++----- 2 files changed, 98 insertions(+), 25 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 53a38d6642..2004da059c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -7,10 +7,12 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext class ApplyTokenListSortingUseCase( @@ -20,21 +22,60 @@ class ApplyTokenListSortingUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - sortedTokens: Set, - isGrouped: Boolean, + sortedTokensIds: Set>, + isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): Either { return withContext(dispatchers.default) { either { - val nonEmptyTokens = ensureNotNull(sortedTokens.toNonEmptySetOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - - applySorting(userWalletId, nonEmptyTokens, isGrouped, isSortedByBalance) + applySorting( + userWalletId = userWalletId, + tokens = sortTokens(sortedTokensIds, getTokens(userWalletId)), + isGrouped = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) } } } + private suspend fun Raise.sortTokens( + sortedTokensIds: Set>, + unsortedTokens: Set, + ): Set = withContext(dispatchers.default) { + val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + + val sortedTokens = sortedMapOf() + + unsortedTokens.forEach { token -> + val index = nonEmptySortedTokensIds.indexOfFirst { (networkId, tokenId) -> + networkId == token.networkId && tokenId == token.id + } + + if (index >= 0) { + sortedTokens[index] = token + } else { + raise(TokenListSortingError.UnableToSortTokenList) + } + } + + ensureNotNull(sortedTokens.values.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + } + + private suspend fun Raise.getTokens(userWalletId: UserWalletId): Set { + val tokens = catch( + block = { tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh = false).firstOrNull() }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + + return ensureNotNull(tokens?.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + } + private suspend fun Raise.applySorting( userWalletId: UserWalletId, tokens: Set, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index 0af91c7c22..6a666e453b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -6,13 +6,17 @@ import arrow.core.right import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.MockTokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.random.Random internal class ApplyTokenListSortingUseCaseTest { @@ -28,8 +32,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokens = emptySet(), - isGrouped = false, + sortedTokensIds = emptySet(), + isGroupedByNetwork = false, isSortedByBalance = false, ) @@ -50,8 +54,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokens = MockTokens.tokens, - isGrouped = false, + sortedTokensIds = MockTokens.tokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = false, isSortedByBalance = false, ) @@ -62,7 +66,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { // Given - val expectedTokens = MockTokens.tokens + val expectedTokens = getSortedTokens() val expectedIsGrouped = true val expectedIsSorted = true @@ -72,8 +76,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokens = expectedTokens, - isGrouped = expectedIsGrouped, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -86,7 +90,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { // Given - val expectedTokens = MockTokens.tokens + val expectedTokens = getSortedTokens() val expectedIsGrouped = true val expectedIsSorted = false @@ -96,8 +100,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokens = expectedTokens, - isGrouped = expectedIsGrouped, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -110,7 +114,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { // Given - val expectedTokens = MockTokens.tokens + val expectedTokens = getSortedTokens() val expectedIsGrouped = false val expectedIsSorted = true @@ -120,8 +124,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokens = expectedTokens, - isGrouped = expectedIsGrouped, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -134,7 +138,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { // Given - val expectedTokens = MockTokens.tokens + val expectedTokens = getSortedTokens() val expectedIsGrouped = false val expectedIsSorted = false @@ -144,8 +148,8 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokens = expectedTokens, - isGrouped = expectedIsGrouped, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -155,12 +159,40 @@ internal class ApplyTokenListSortingUseCaseTest { assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) } + @Test + fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.UnableToSortTokenList.left() + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokensIds = getSortedTokens().drop(n = 3).map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + private fun getSortedTokens() = MockTokens.tokens + .sortedBy { Random.nextInt(0, MockTokens.tokens.size) } + .toSet() + private fun getUseCase(tokensRepository: MockTokensRepository = getTokensRepository()) = ApplyTokenListSortingUseCase( tokensRepository = tokensRepository, dispatchers = TestingCoroutineDispatcherProvider(), ) - private fun getTokensRepository(sortTokensResult: Either = Unit.right()) = - MockTokensRepository(sortTokensResult, emptyFlow(), emptyFlow(), emptyFlow()) + private fun getTokensRepository( + sortTokensResult: Either = Unit.right(), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + ): MockTokensRepository { + return MockTokensRepository(sortTokensResult, tokens, emptyFlow(), emptyFlow()) + } } \ No newline at end of file From 160153bfa30a4883ea31a0ab6e1d56a4d88a72ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Aug 2023 15:25:12 +0800 Subject: [PATCH 082/100] Updated on 2026-08-14 --- app/build.gradle.kts | 5 +- .../tap/di/domain/TxHistoryDomainModule.kt | 28 +++ .../usecase/GetTxHistoryItemsCountUseCase.kt | 10 +- .../usecase/GetTxHistoryItemsUseCase.kt | 35 +--- features/wallet/impl/build.gradle.kts | 10 +- .../presentation/common/WalletPreviewData.kt | 144 ++++++++------- .../wallet/state/WalletContentItemState.kt | 57 ------ .../wallet/state/WalletStateHolder.kt | 27 ++- .../state/content/WalletLockedContentState.kt | 9 + .../state/content/WalletTokensListState.kt | 60 +++++++ .../state/content/WalletTxHistoryState.kt | 92 ++++++++++ .../WalletLoadedTokensListConverter.kt | 7 +- .../factory/WalletLoadedTxHistoryConverter.kt | 130 ++++++++++++++ .../WalletLoadingTxHistoryConverter.kt | 103 +++++++++++ .../factory/WalletSkeletonStateConverter.kt | 22 +-- .../state/factory/WalletStateFactory.kt | 41 ++++- .../presentation/wallet/ui/WalletScreen.kt | 166 ++++++++++-------- .../multicurrency/MultiCurrencyContentItem.kt | 27 +++ .../multicurrency/OrganizeTokensButton.kt | 29 +++ .../SingleCurrencyContentItem.kt | 29 +++ ...ckGroupTitle.kt => TxHistoryGroupTitle.kt} | 11 +- ...actionsBlockTitle.kt => TxHistoryTitle.kt} | 15 +- .../wallet/utils/LoadingItemsProvider.kt | 14 +- .../TokenListErrorToWalletStateConverter.kt | 16 +- .../utils/TokenListToContentItemsConverter.kt | 45 +++-- .../utils/TokenListToWalletStateConverter.kt | 60 ++++--- .../WalletNotificationsListFactory.kt | 12 +- .../wallet/viewmodels/WalletViewModel.kt | 77 +++++--- 28 files changed, 913 insertions(+), 368 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/{TransactionsBlockGroupTitle.kt => TxHistoryGroupTitle.kt} (73%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/{TransactionsBlockTitle.kt => TxHistoryTitle.kt} (81%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e9c2b9786..de202bea18 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(project(":domain:wallets:models")) implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(projects.domain.txhistory) implementation(project(":common")) implementation(project(":core:analytics")) @@ -43,11 +44,13 @@ dependencies { implementation(project(":core:utils")) implementation(project(":libs:crypto")) implementation(project(":libs:auth")) + implementation(project(":data:source:preferences")) implementation(projects.data.card) + implementation(projects.data.common) implementation(projects.data.settings) implementation(projects.data.tokens) - implementation(projects.data.common) + implementation(projects.data.txhistory) /** Features */ implementation(project(":features:onboarding")) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt new file mode 100644 index 0000000000..3ab36cd231 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.tokens.* +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TxHistoryDomainModule { + + @Provides + @ViewModelScoped + fun provideGetTxHistoryItemsCountUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsCountUseCase { + return GetTxHistoryItemsCountUseCase(repository = txHistoryRepository) + } + + @Provides + @ViewModelScoped + fun provideGetTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsUseCase { + return GetTxHistoryItemsUseCase(repository = txHistoryRepository) + } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index c6c0b347b3..1ecdc72dd6 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -12,12 +12,12 @@ class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) return either { catch( block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, - catch = { e -> + catch = { throwable -> raise( - when (e) { - is TxHistoryStateError.TxHistoryNotImplemented -> e - is TxHistoryStateError.EmptyTxHistories -> e - else -> TxHistoryStateError.DataError(e) + when (throwable) { + is TxHistoryStateError.TxHistoryNotImplemented -> throwable + is TxHistoryStateError.EmptyTxHistories -> throwable + else -> TxHistoryStateError.DataError(throwable) }, ) }, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 48d2759704..9f3fc2fbe0 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -2,17 +2,12 @@ package com.tangem.domain.txhistory.usecase import androidx.paging.PagingData import arrow.core.Either -import arrow.core.left -import arrow.core.raise.Raise -import arrow.core.raise.recover -import arrow.core.right +import arrow.core.raise.either import com.tangem.domain.txhistory.error.TxHistoryListError import com.tangem.domain.txhistory.model.TxHistoryItem import com.tangem.domain.txhistory.repository.TxHistoryRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collect private const val DEFAULT_PAGE_SIZE = 20 @@ -21,29 +16,11 @@ class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { operator fun invoke( networkId: String, pageSize: Int = DEFAULT_PAGE_SIZE, - ): Flow,>,> { - return channelFlow { - recover( - block = { - getTxHistoryItems( - networkId = networkId, - pageSize = pageSize, - ).collect { items -> - send(items.right()) - } - }, - recover = { error -> send(error.left()) }, - ) + ): Either>> { + return either { + repository + .getTxHistoryItems(networkId = networkId, pageSize = pageSize) + .catch { raise(TxHistoryListError.DataError(it)) } } } - - private fun Raise.getTxHistoryItems( - networkId: String, - pageSize: Int, - ): Flow> { - return repository - .getTxHistoryItems(networkId = networkId, pageSize = pageSize) - .catch { raise(TxHistoryListError.DataError(it)) } - } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 60ca50d490..25b400217f 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -12,18 +12,19 @@ dependencies { implementation(deps.material) /** Compose */ + implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) - implementation(deps.compose.material) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) + implementation(deps.compose.reorderable) + implementation(deps.compose.shimmer) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.shimmer) - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.reorderable) /** Other libraries */ implementation(deps.arrow.core) @@ -50,6 +51,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(projects.domain.txhistory) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 5dfb53248c..722d10e9cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.common +import androidx.paging.PagingData import com.tangem.core.ui.R import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig @@ -11,8 +12,11 @@ import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.flowOf import java.util.UUID internal object WalletPreviewData { @@ -221,54 +225,57 @@ internal object WalletPreviewData { onBackClick = {}, topBarConfig = walletTopBarConfig, walletsListConfig = walletListConfig, - contentItems = persistentListOf( - WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle("Bitcoin"), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_1", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_2", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_3", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_4", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle("Ethereum"), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_5", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", + tokensListState = WalletTokensListState.Content( + persistentListOf( + WalletTokensListState.TokensListItemState.NetworkGroupTitle("Bitcoin"), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_1", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_2", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_3", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_4", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.NetworkGroupTitle("Ethereum"), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_5", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), ), ), + onOrganizeTokensClick = {}, ), pullToRefreshConfig = WalletPullToRefreshConfig( isRefreshing = false, @@ -281,32 +288,12 @@ internal object WalletPreviewData { WalletNotification.ScanCard(onClick = {}), ), bottomSheet = bottomSheet, - onOrganizeTokensClick = {}, ) val singleWalletScreenState = WalletStateHolder.SingleCurrencyContent( onBackClick = {}, topBarConfig = walletTopBarConfig, walletsListConfig = walletListConfig, - contentItems = persistentListOf( - WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - WalletContentItemState.SingleCurrencyItem.GroupTitle("Today"), - WalletContentItemState.SingleCurrencyItem.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", - ), - ), - WalletContentItemState.SingleCurrencyItem.GroupTitle("Yesterday"), - WalletContentItemState.SingleCurrencyItem.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", - ), - ), - ), pullToRefreshConfig = WalletPullToRefreshConfig( isRefreshing = false, onRefresh = {}, @@ -322,5 +309,30 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), + txHistoryState = WalletTxHistoryState.Content( + flowOf( + PagingData.from( + listOf( + WalletTxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), + WalletTxHistoryState.TxHistoryItemState.GroupTitle("Today"), + WalletTxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), + ), + WalletTxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), + WalletTxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), + ), + ), + ), + ), + ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt deleted file mode 100644 index 19142be1cd..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state - -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState - -/** - * Wallet screen content item state - * -[REDACTED_AUTHOR] - */ -internal sealed interface WalletContentItemState { - - /** Multi currency wallet content state */ - sealed interface MultiCurrencyItem : WalletContentItemState { - - /** - * Network group title item - * - * @property networkName network name - */ - data class NetworkGroupTitle(val networkName: String) : MultiCurrencyItem - - /** - * Token item - * - * @property state token item state - */ - data class Token(val state: TokenItemState) : MultiCurrencyItem - } - - /** Single currency wallet content state */ - sealed interface SingleCurrencyItem : WalletContentItemState { - - /** - * Title item - * - * @property onExploreClick lambda be invoke when explore button was clicked - */ - data class Title(val onExploreClick: () -> Unit) : SingleCurrencyItem - - /** - * Group title item - * - * @property title title - */ - data class GroupTitle(val title: String) : SingleCurrencyItem - - /** - * Transaction item - * - * @property state transaction state - */ - data class Transaction(val state: TransactionState) : SingleCurrencyItem - } - - object Loading : WalletContentItemState -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index 169139dacd..216dc94148 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -3,6 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletLockedContentState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -13,7 +16,6 @@ import kotlinx.collections.immutable.persistentListOf * @property topBarConfig top bar config * @property walletsListConfig wallets list config * @property pullToRefreshConfig pull to refresh config - * @property contentItems content items * @property notifications notifications * [REDACTED_AUTHOR] @@ -23,7 +25,6 @@ internal sealed class WalletStateHolder( open val topBarConfig: WalletTopBarConfig, open val walletsListConfig: WalletsListConfig, open val pullToRefreshConfig: WalletPullToRefreshConfig, - open val contentItems: ImmutableList, open val notifications: ImmutableList, open val bottomSheet: WalletBottomSheetConfig? = null, ) { @@ -33,7 +34,6 @@ internal sealed class WalletStateHolder( topBarConfig: WalletTopBarConfig = this.topBarConfig, walletsListConfig: WalletsListConfig = this.walletsListConfig, pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, - contentItems: ImmutableList = this.contentItems, notifications: ImmutableList = this.notifications, bottomSheet: WalletBottomSheetConfig? = this.bottomSheet, ): WalletStateHolder { @@ -43,7 +43,6 @@ internal sealed class WalletStateHolder( topBarConfig = topBarConfig, walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, - contentItems = contentItems as ImmutableList, notifications = notifications, bottomSheet = bottomSheet, ) @@ -52,7 +51,6 @@ internal sealed class WalletStateHolder( topBarConfig = topBarConfig, walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, - contentItems = contentItems as ImmutableList, notifications = notifications, bottomSheet = bottomSheet, ) @@ -73,27 +71,24 @@ internal sealed class WalletStateHolder( * @property topBarConfig top bar config * @property walletsListConfig wallets list config * @property pullToRefreshConfig pull to refresh config - * @property contentItems content items + * @property tokensListState token list state * @property notifications notifications - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked */ data class MultiCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val contentItems: ImmutableList, override val notifications: ImmutableList, override val bottomSheet: WalletBottomSheetConfig? = null, - val onOrganizeTokensClick: () -> Unit, + val tokensListState: WalletTokensListState, ) : WalletStateHolder( onBackClick = onBackClick, topBarConfig = topBarConfig, walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, - contentItems = contentItems, - bottomSheet = bottomSheet, notifications = notifications, + bottomSheet = bottomSheet, ) /** @@ -103,27 +98,26 @@ internal sealed class WalletStateHolder( * @property topBarConfig top bar config * @property walletsListConfig wallets list config * @property pullToRefreshConfig pull to refresh config - * @property contentItems content items * @property notifications notifications * @property buttons manage buttons * @property marketPriceBlockState market price block state + * @property txHistoryState transactions history state */ data class SingleCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val contentItems: ImmutableList, override val notifications: ImmutableList, override val bottomSheet: WalletBottomSheetConfig? = null, val buttons: ImmutableList, val marketPriceBlockState: MarketPriceBlockState, + val txHistoryState: WalletTxHistoryState, ) : WalletStateHolder( onBackClick = onBackClick, topBarConfig = topBarConfig, walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, - contentItems = contentItems, notifications = notifications, bottomSheet = bottomSheet, ) @@ -134,6 +128,8 @@ internal sealed class WalletStateHolder( * @property onBackClick lambda be invoked when back button is clicked * @property topBarConfig top bar config * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property lockedContentState locked content state * @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked * @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed * @property onUnlockClick lambda be invoked when unlock button is clicked @@ -144,6 +140,7 @@ internal sealed class WalletStateHolder( override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, + val lockedContentState: WalletLockedContentState, val onUnlockWalletsNotificationClick: () -> Unit, val onBottomSheetDismissRequest: () -> Unit, val onUnlockClick: () -> Unit, @@ -153,7 +150,6 @@ internal sealed class WalletStateHolder( topBarConfig = topBarConfig, walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, - contentItems = persistentListOf(WalletContentItemState.Loading), notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)), bottomSheet = WalletBottomSheetConfig( isShow = false, @@ -186,7 +182,6 @@ internal sealed class WalletStateHolder( onWalletChange = {}, ), pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}), - contentItems = persistentListOf(WalletContentItemState.Loading), notifications = persistentListOf(), bottomSheet = null, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt new file mode 100644 index 0000000000..9867aed606 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +/** + * Wallet locked content state. + * It allows to divide the locked content of multi-currency and single-currency wallets. + * +[REDACTED_AUTHOR] + */ +internal sealed interface WalletLockedContentState \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt new file mode 100644 index 0000000000..4f7e51d4d3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Wallet tokens list state + * + * @property items content items + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize strings [REDACTED_JIRA] +internal sealed class WalletTokensListState( + open val items: ImmutableList, + open val onOrganizeTokensClick: (() -> Unit)?, +) { + + /** + * Content state + * + * @property items content items + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + */ + data class Content( + override val items: ImmutableList, + override val onOrganizeTokensClick: (() -> Unit)?, + ) : WalletTokensListState(items, onOrganizeTokensClick) + + /** Locked content state */ + object Locked : + WalletTokensListState( + items = persistentListOf( + TokensListItemState.NetworkGroupTitle(networkName = "Tokens"), + TokensListItemState.Token(state = TokenItemState.Loading), + ), + onOrganizeTokensClick = null, + ), + WalletLockedContentState + + /** Tokens list item state */ + sealed interface TokensListItemState { + + /** + * Network group title item + * + * @property networkName network name + */ + data class NetworkGroupTitle(val networkName: String) : TokensListItemState + + /** + * Token item + * + * @property state token item state + */ + data class Token(val state: TokenItemState) : TokensListItemState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt new file mode 100644 index 0000000000..f9b3c822ee --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +import androidx.paging.PagingData +import com.tangem.core.ui.components.transactions.TransactionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * Wallet transaction history state + * +[REDACTED_AUTHOR] + */ +internal sealed interface WalletTxHistoryState { + + /** + * Wallet transaction history state with content + * + * @property items content items + */ + sealed class ContentItemsState(open val items: Flow>) : WalletTxHistoryState + + /** + * Content state + * + * @property items content items + */ + data class Content(override val items: Flow>) : ContentItemsState(items) + + /** + * Empty state + * + * @property onBuyClick lambda be invoke when buy button was clicked + */ + data class Empty(val onBuyClick: () -> Unit) : WalletTxHistoryState + + /** + * Not supported tx history state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class NotSupported(val onExploreClick: () -> Unit) : WalletTxHistoryState + + /** + * Error state + * + * @property onReloadClick lambda be invoke when reload button was clicked + */ + data class Error(val onReloadClick: () -> Unit) : WalletTxHistoryState + + /** + * Locked state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Locked(val onExploreClick: () -> Unit) : + ContentItemsState( + items = flowOf( + PagingData.from( + listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction(state = TransactionState.Loading), + ), + ), + ), + ), + WalletLockedContentState + + /** Transactions history item state */ + sealed interface TxHistoryItemState { + + /** + * Title item + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Title(val onExploreClick: () -> Unit) : TxHistoryItemState + + /** + * Group title item + * + * @property title title + */ + data class GroupTitle(val title: String) : TxHistoryItemState + + /** + * Transaction item + * + * @property state transaction state + */ + data class Transaction(val state: TransactionState) : TxHistoryItemState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index 138f36b6fd..edcbbdd1e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks import com.tangem.utils.converter.Converter /** @@ -19,17 +20,19 @@ import com.tangem.utils.converter.Converter */ internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, + clickCallbacks: WalletClickCallbacks, ) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( - currentState = currentStateProvider(), + currentStateProvider = currentStateProvider, isWalletContentHidden = false, // TODO: [REDACTED_JIRA] fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + clickCallbacks = clickCallbacks, ) private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter( - currentState = currentStateProvider(), + currentStateProvider = currentStateProvider, ) override fun convert(value: LoadedTokensListModel): WalletStateHolder { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..1d8fa010d8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt @@ -0,0 +1,130 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import androidx.paging.PagingData +import androidx.paging.map +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Converter from loaded tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, +) : Converter>>, WalletStateHolder> { + + override fun convert(value: Either>>): WalletStateHolder { + return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + } + + private fun convertError(): WalletStateHolder { + val state = currentStateProvider() + return WalletStateHolder.SingleCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + buttons = persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList(), + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = currentCardTypeResolverProvider().getBlockchain().currency, + ), + // TODO: [REDACTED_JIRA] + txHistoryState = WalletTxHistoryState.Error(onReloadClick = {}), + ) + } + + private fun convert(items: Flow>): WalletStateHolder { + val state = currentStateProvider() + return WalletStateHolder.SingleCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + buttons = persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList(), + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = currentCardTypeResolverProvider().getBlockchain().currency, + ), + txHistoryState = createContentTxHistory(items), + ) + } + + private fun createContentTxHistory(items: Flow>): WalletTxHistoryState { + return WalletTxHistoryState.Content( + items = items.map { pagingData -> + pagingData.map { item -> + TxHistoryItemState.Transaction( + state = when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + address = direction.from, + amount = item.amount.toPlainString(), + timestamp = item.timestamp.toString(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + address = direction.from, + amount = item.amount.toPlainString(), + timestamp = item.timestamp.toString(), + ) + } + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + address = direction.to, + amount = item.amount.toPlainString(), + timestamp = item.timestamp.toString(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + address = direction.to, + amount = item.amount.toPlainString(), + timestamp = item.timestamp.toString(), + ) + } + } + }, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..b24b26dd5a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt @@ -0,0 +1,103 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flowOf + +/** + * Converter from loading tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, +) : Converter, WalletStateHolder> { + + override fun convert(value: Either): WalletStateHolder { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convert(value: Int): WalletStateHolder { + val state = currentStateProvider() + + return WalletStateHolder.SingleCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + buttons = persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList(), + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = currentCardTypeResolverProvider().getBlockchain().currency, + ), + txHistoryState = createLoadingTxHistory(itemCount = value), + ) + } + + private fun createLoadingTxHistory(itemCount: Int): WalletTxHistoryState { + return WalletTxHistoryState.Content( + items = flowOf( + value = PagingData.from( + data = buildList( + capacity = itemCount, + builderAction = { + add( + element = WalletTxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Loading, + ), + ) + }, + ), + ), + ), + ) + } + + private fun convertError(error: TxHistoryStateError): WalletStateHolder { + val state = currentStateProvider() + + return WalletStateHolder.SingleCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + buttons = WalletPreviewData.singleWalletScreenState.buttons, + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = currentCardTypeResolverProvider().getBlockchain().currency, + ), + // TODO: [REDACTED_JIRA] + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> WalletTxHistoryState.Empty(onBuyClick = {}) + is TxHistoryStateError.DataError -> WalletTxHistoryState.Error(onReloadClick = {}) + is TxHistoryStateError.TxHistoryNotImplemented -> WalletTxHistoryState.NotSupported(onExploreClick = {}) + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index e372cb1500..dfdf7abdb3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -1,17 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory +import androidx.paging.PagingData import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flow /** * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder] @@ -40,10 +43,12 @@ internal class WalletSkeletonStateConverter( topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), - contentItems = persistentListOf(), + tokensListState = WalletTokensListState.Content( + items = persistentListOf(), + onOrganizeTokensClick = clickCallbacks::onOrganizeTokensClick, + ), notifications = persistentListOf(), bottomSheet = null, - onOrganizeTokensClick = clickCallbacks::onOrganizeTokensClick, ) } @@ -56,13 +61,15 @@ internal class WalletSkeletonStateConverter( topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), - contentItems = persistentListOf(), notifications = persistentListOf(), bottomSheet = null, buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons marketPriceBlockState = MarketPriceBlockState.Loading( currencyName = cardTypeResolver.getBlockchain().currency, ), + txHistoryState = WalletTxHistoryState.Content( + items = flow { PagingData.empty() }, + ), ) } @@ -78,12 +85,7 @@ internal class WalletSkeletonStateConverter( selectedWalletIndex = 0, wallets = wallets.map { wallet -> WalletCardState.Loading( - // TODO remove after adding release logic for GetTokenListUseCase - id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { - UserWalletId("123") - } else { - UserWalletId("321") - }, + id = wallet.walletId, title = wallet.name, additionalInfo = "", // TODO add additional info resolver imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 3aec8d0359..e4e0952491 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -1,9 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory +import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.model.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification @@ -11,6 +16,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow /** * Main factory for creating [WalletStateHolder] @@ -20,11 +26,32 @@ import kotlinx.collections.immutable.ImmutableList */ internal class WalletStateFactory( private val currentStateProvider: Provider, + currentCardTypeResolverProvider: Provider, private val clickCallbacks: WalletClickCallbacks, ) { - private val skeletonConverter = WalletSkeletonStateConverter(clickCallbacks = clickCallbacks) - private val loadedTokensListConverter = WalletLoadedTokensListConverter(currentStateProvider = currentStateProvider) + private val skeletonConverter by lazy { WalletSkeletonStateConverter(clickCallbacks = clickCallbacks) } + + private val loadedTokensListConverter by lazy { + WalletLoadedTokensListConverter( + currentStateProvider = currentStateProvider, + clickCallbacks = clickCallbacks, + ) + } + + private val loadingTransactionsStateConverter by lazy { + WalletLoadingTxHistoryConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + ) + } + + private val loadedTxHistoryConverter by lazy { + WalletLoadedTxHistoryConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + ) + } fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickCallbacks::onBackClick) @@ -66,4 +93,14 @@ internal class WalletStateFactory( ) } } + + fun getLoadingTxHistoryState(itemsCountEither: Either): WalletStateHolder { + return loadingTransactionsStateConverter.convert(value = itemsCountEither) + } + + fun getLoadedTxHistoryState( + txHistoryEither: Either>>, + ): WalletStateHolder { + return loadedTxHistoryConverter.convert(txHistoryEither) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index c777e06a49..1ba7a134d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -4,10 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshState @@ -17,29 +14,26 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemsIndexed import com.tangem.core.ui.components.buttons.HorizontalActionChips -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.transactions.Transaction import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem -import com.tangem.feature.wallet.presentation.common.component.TokenItem -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.MultiCurrencyContentItem +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.OrganizeTokensButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.SingleCurrencyContentItem import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -74,6 +68,16 @@ internal fun WalletScreen(state: WalletStateHolder) { .padding(paddingValues = scaffoldPaddings) .pullRefresh(pullRefreshState), ) { + val txHistoryItems = if (state is WalletStateHolder.SingleCurrencyContent) { + if (state.txHistoryState is WalletTxHistoryState.ContentItemsState) { + state.txHistoryState.items.collectAsLazyPagingItems() + } else { + null + } + } else { + null + } + LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), @@ -90,8 +94,7 @@ internal fun WalletScreen(state: WalletStateHolder) { item { HorizontalActionChips( buttons = state.buttons, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14), + modifier = changeableItemModifier.padding(top = TangemTheme.dimens.spacing14), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) } @@ -120,33 +123,12 @@ internal fun WalletScreen(state: WalletStateHolder) { } } - itemsIndexed( - items = state.contentItems, - key = { index, item -> - when (item) { - is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName - is WalletContentItemState.MultiCurrencyItem.Token -> index - is WalletContentItemState.SingleCurrencyItem.Title -> index - is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title - is WalletContentItemState.SingleCurrencyItem.Transaction -> index - is WalletContentItemState.Loading -> index - } - }, - itemContent = { index, item -> - ContentItem( - item = item, - modifier = changeableItemModifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = state.contentItems.lastIndex, - ), - ) - }, - ) + contentItems(state = state, txHistoryItems = txHistoryItems, modifier = changeableItemModifier) if (state is WalletStateHolder.MultiCurrencyContent) { item { OrganizeTokensButton( - onClick = state.onOrganizeTokensClick, + onClick = state.tokensListState.onOrganizeTokensClick, modifier = changeableItemModifier .padding(top = TangemTheme.dimens.spacing14) .padding(horizontal = TangemTheme.dimens.spacing16), @@ -173,6 +155,74 @@ internal fun WalletScreen(state: WalletStateHolder) { } } +private fun LazyListScope.contentItems( + state: WalletStateHolder, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletStateHolder.MultiCurrencyContent -> { + tokensListItems(state = state.tokensListState, modifier = modifier) + } + is WalletStateHolder.SingleCurrencyContent -> { + txHistoryItems( + state = state.txHistoryState, + txHistoryItems = txHistoryItems, + modifier = modifier, + ) + } + is WalletStateHolder.Loading, + is WalletStateHolder.UnlockWalletContent, + -> Unit + } +} + +private fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { + itemsIndexed( + items = state.items, + key = { index, _ -> index }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + ), + ) + }, + ) +} + +private fun LazyListScope.txHistoryItems( + state: WalletTxHistoryState, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletTxHistoryState.ContentItemsState -> { + checkNotNull(txHistoryItems) + itemsIndexed( + items = txHistoryItems, + key = { index, _ -> index }, + itemContent = { index, item -> + if (item == null) return@itemsIndexed + + SingleCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = txHistoryItems.itemCount, + ), + ) + }, + ) + } + is WalletTxHistoryState.Empty -> TODO("[REDACTED_JIRA]") + is WalletTxHistoryState.Error -> TODO("[REDACTED_JIRA]") + is WalletTxHistoryState.NotSupported -> TODO("[REDACTED_JIRA]") + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) { @@ -183,42 +233,6 @@ private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshStat ) } -@Composable -private fun ContentItem(item: WalletContentItemState, modifier: Modifier = Modifier) { - when (item) { - is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> { - NetworkGroupItem(networkName = item.networkName, modifier = modifier) - } - is WalletContentItemState.MultiCurrencyItem.Token -> { - TokenItem(state = item.state, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.Title -> { - TransactionsBlockTitle(config = item, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.GroupTitle -> { - TransactionsBlockGroupTitle(config = item, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.Transaction -> { - Transaction(state = item.state, modifier = modifier) - } - WalletContentItemState.Loading -> { - TokenItem(state = TokenItemState.Loading, modifier = modifier) - } - } -} - -@Composable -private fun OrganizeTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - RoundedActionButton( - config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_title), - iconResId = R.drawable.ic_filter_24, - onClick = onClick, - ), - modifier = modifier, - ) -} - // region Preview @Preview @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt new file mode 100644 index 0000000000..a387c5f956 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem +import com.tangem.feature.wallet.presentation.common.component.TokenItem +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState + +/** + * Multi-currency content item + * + * @param state item state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { + when (state) { + is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { + NetworkGroupItem(networkName = state.networkName, modifier = modifier) + } + is WalletTokensListState.TokensListItemState.Token -> { + TokenItem(state = state.state, modifier = modifier) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt new file mode 100644 index 0000000000..f7e77273fa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.feature.wallet.impl.R + +/** + * Organize tokens button + * + * @param onClick callback, if null button is disabled + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { + RoundedActionButton( + config = ActionButtonConfig( + text = stringResource(id = R.string.organize_tokens_title), + iconResId = R.drawable.ic_filter_24, + onClick = onClick ?: {}, + enabled = onClick != null, + ), + modifier = modifier, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt new file mode 100644 index 0000000000..58489e3a3d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.transactions.Transaction +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState + +/** + * Single currency content item + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun SingleCurrencyContentItem(state: WalletTxHistoryState.TxHistoryItemState, modifier: Modifier = Modifier) { + when (state) { + is WalletTxHistoryState.TxHistoryItemState.GroupTitle -> { + TxHistoryGroupTitle(config = state, modifier = modifier) + } + is WalletTxHistoryState.TxHistoryItemState.Title -> { + TxHistoryTitle(config = state, modifier = modifier) + } + is WalletTxHistoryState.TxHistoryItemState.Transaction -> { + Transaction(state = state.state, modifier = modifier) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt similarity index 73% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt index eed98b7ebf..275f2fce6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block group title @@ -18,10 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemStat * @param modifier modifier */ @Composable -internal fun TransactionsBlockGroupTitle( - config: WalletContentItemState.SingleCurrencyItem.GroupTitle, - modifier: Modifier = Modifier, -) { +internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) { Text( text = config.title, modifier = modifier @@ -41,7 +38,7 @@ internal fun TransactionsBlockGroupTitle( @Composable private fun Preview_TransactionsBlockGroupTitle_Light() { TangemTheme(isDark = false) { - TransactionsBlockGroupTitle(config = WalletContentItemState.SingleCurrencyItem.GroupTitle(title = "Today")) + TxHistoryGroupTitle(config = TxHistoryItemState.GroupTitle(title = "Today")) } } @@ -49,6 +46,6 @@ private fun Preview_TransactionsBlockGroupTitle_Light() { @Composable private fun Preview_TransactionsBlockGroupTitle_Dark() { TangemTheme(isDark = true) { - TransactionsBlockGroupTitle(config = WalletContentItemState.SingleCurrencyItem.GroupTitle(title = "Today")) + TxHistoryGroupTitle(config = TxHistoryItemState.GroupTitle(title = "Today")) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt similarity index 81% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt index 58e675f1fb..a71ec939db 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block title @@ -21,10 +21,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemStat * @param modifier modifier */ @Composable -internal fun TransactionsBlockTitle( - config: WalletContentItemState.SingleCurrencyItem.Title, - modifier: Modifier = Modifier, -) { +internal fun TxHistoryTitle(config: TxHistoryItemState.Title, modifier: Modifier = Modifier) { Row( modifier = modifier .background(TangemTheme.colors.background.primary) @@ -62,9 +59,7 @@ internal fun TransactionsBlockTitle( @Composable private fun Preview_TransactionsBlockTitle_Light() { TangemTheme(isDark = false) { - TransactionsBlockTitle( - config = WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - ) + TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) } } @@ -72,8 +67,6 @@ private fun Preview_TransactionsBlockTitle_Light() { @Composable private fun Preview_TransactionsBlockTitle_Dark() { TangemTheme(isDark = true) { - TransactionsBlockTitle( - config = WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - ) + TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt index c00f864c32..067db48037 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList internal object LoadingItemsProvider { - fun getLoadingMultiCurrencyTokens(): PersistentList { - return List(size = 5) { TokenItemState.Loading } - .map { MultiCurrencyItem.Token(it) } - .toPersistentList() + fun getLoadingMultiCurrencyTokens(): ImmutableList { + return buildList(capacity = 5) { + add(WalletTokensListState.TokensListItemState.Token(state = TokenItemState.Loading)) + }.toImmutableList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt index d6f3112be9..b93109b6f6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt @@ -1,15 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.utils +import com.tangem.common.Provider import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf internal class TokenListErrorToWalletStateConverter( - private val currentState: WalletStateHolder, + private val currentStateProvider: Provider, ) : Converter { // TODO: [REDACTED_JIRA] override fun convert(value: TokenListError): WalletStateHolder { - return currentState + val state = currentStateProvider() + return WalletStateHolder.MultiCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index e488aa7b7f..f4f1d3803f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -3,10 +3,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState.TokensListItemState import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf @@ -15,36 +16,44 @@ internal class TokenListToContentItemsConverter( isWalletContentHidden: Boolean, fiatCurrencyCode: String, fiatCurrencySymbol: String, -) : Converter> { + private val clickCallbacks: WalletClickCallbacks, +) : Converter { private val tokenStatusConverter = TokenStatusToTokenItemConverter( - isWalletContentHidden, - fiatCurrencyCode, - fiatCurrencySymbol, + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, ) - override fun convert(value: TokenList): ImmutableList { - return when (value) { - is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() - is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() - is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() - } + override fun convert(value: TokenList): WalletTokensListState { + return WalletTokensListState.Content( + items = when (value) { + is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() + is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() + is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() + }, + onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) { + clickCallbacks::onOrganizeTokensClick + } else { + null + }, + ) } - private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList { + private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList { return groups.fold(initial = persistentListOf()) { acc, group -> acc.mutate { it.addGroup(group) } } } - private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList { + private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList { return tokens.fold(initial = persistentListOf()) { acc, token -> acc.mutate { it.addToken(token) } } } - private fun MutableList.addGroup(group: NetworkGroup): List { - this.add(MultiCurrencyItem.NetworkGroupTitle(group.network.name)) + private fun MutableList.addGroup(group: NetworkGroup): List { + this.add(TokensListItemState.NetworkGroupTitle(group.network.name)) group.tokens.forEach { token -> this.addToken(token) @@ -53,10 +62,10 @@ internal class TokenListToContentItemsConverter( return this } - private fun MutableList.addToken(token: TokenStatus): List { + private fun MutableList.addToken(token: TokenStatus): List { val tokenItemState = tokenStatusConverter.convert(token) - this.add(MultiCurrencyItem.Token(tokenItemState)) + this.add(TokensListItemState.Token(tokenItemState)) return this } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index b1374bbead..056bbb3d1d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -1,48 +1,56 @@ package com.tangem.feature.wallet.presentation.wallet.utils -import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.common.Provider import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList internal class TokenListToWalletStateConverter( - private val currentState: WalletStateHolder, + private val currentStateProvider: Provider, private val isWalletContentHidden: Boolean, private val fiatCurrencyCode: String, private val fiatCurrencySymbol: String, + clickCallbacks: WalletClickCallbacks, ) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, fiatCurrencyCode = fiatCurrencyCode, fiatCurrencySymbol = fiatCurrencySymbol, + clickCallbacks = clickCallbacks, ) override fun convert(value: TokensListModel): WalletStateHolder { - val state = if (currentState is MultiCurrencyContent) { - currentState.updateWithTokenList(tokenList = value.tokenList) - } else { - currentState - } - - return state.copySealed( - walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance), - pullToRefreshConfig = if (value.isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) - } else { - state.pullToRefreshConfig - }, - ) + val state = currentStateProvider() + return state + .updateWithTokenList(tokenList = value.tokenList) + .copySealed( + walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance), + pullToRefreshConfig = if (value.isRefreshing) { + state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) + } else { + state.pullToRefreshConfig + }, + ) } - private fun MultiCurrencyContent.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { - return this.copy(contentItems = tokenListToContentConverter.convert(value = tokenList)) + private fun WalletStateHolder.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { + return MultiCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + tokensListState = tokenListToContentConverter.convert(value = tokenList), + ) } private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { @@ -63,13 +71,13 @@ internal class TokenListToWalletStateConverter( } private fun WalletStateHolder.getRefreshingStatus(): Boolean { - return contentItems.any { state -> - val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token - val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction - - isMultiCurrencyItem?.state is TokenItemState.Loading || - isSingleCurrencyItem?.state is TransactionState.Loading || - state is WalletContentItemState.Loading + return if (this is MultiCurrencyContent) { + tokensListState.items.any { tokensListItemState -> + tokensListItemState is WalletTokensListState.TokensListItemState.Token && + tokensListItemState.state is TokenItemState.Loading + } + } else { + false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index d0df6d7f3a..a5bbac817e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -6,9 +6,9 @@ import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -32,7 +32,7 @@ internal class WalletNotificationsListFactory( private val clickCallbacks: WalletClickCallbacks, ) { - fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList): Flow> { + fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList?): Flow> { // TODO: [REDACTED_JIRA] order return flow { emit( @@ -63,7 +63,7 @@ internal class WalletNotificationsListFactory( add(element = WalletNotification.BackupCard(onClick = clickCallbacks::onBackupCardClick)) } - if (tokenList.hasMissedDerivations()) { + if (tokenList != null && tokenList.hasMissedDerivations()) { add(element = WalletNotification.ScanCard(onClick = clickCallbacks::onScanCardClick)) } @@ -112,12 +112,12 @@ internal class WalletNotificationsListFactory( } private fun hasUnreachableNetworks(): Boolean { - val isUnreachableState = { item: WalletContentItemState.MultiCurrencyItem -> - (item as? WalletContentItemState.MultiCurrencyItem.Token)?.state is TokenItemState.Unreachable + val isUnreachableState = { item: WalletTokensListState.TokensListItemState -> + (item as? WalletTokensListState.TokensListItemState.Token)?.state is TokenItemState.Unreachable } return currentStateProvider().let { state -> - state is WalletStateHolder.MultiCurrencyContent && state.contentItems.any(isUnreachableState) + state is WalletStateHolder.MultiCurrencyContent && state.tokensListState.items.any(isUnreachableState) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 13b9716f84..3b008c9023 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -14,8 +15,10 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -46,6 +49,8 @@ internal class WalletViewModel @Inject constructor( private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardProcessor: ScanCardProcessor, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickCallbacks { @@ -62,6 +67,9 @@ internal class WalletViewModel @Inject constructor( private val stateFactory = WalletStateFactory( currentStateProvider = Provider { uiState }, + currentCardTypeResolverProvider = Provider { + getCardTypeResolver(index = uiState.walletsListConfig.selectedWalletIndex) + }, clickCallbacks = this, ) @@ -69,10 +77,10 @@ internal class WalletViewModel @Inject constructor( var uiState: WalletStateHolder by mutableStateOf(stateFactory.getInitialState()) private set - private var cardTypeResolverMap: Map by Delegates.notNull() + private var wallets: List by Delegates.notNull() - private val getTokensJobHolder = JobHolder() - private val getNotificationJobHolder = JobHolder() + private val tokensJobHolder = JobHolder() + private val notificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { getWalletsUseCase() @@ -80,15 +88,7 @@ internal class WalletViewModel @Inject constructor( .distinctUntilChanged() .onEach { wallets -> if (wallets.isEmpty()) return@onEach - - cardTypeResolverMap = wallets.associate { wallet -> - // TODO remove after adding release logic for GetTokenListUseCase - if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { - UserWalletId("123") - } else { - UserWalletId("321") - } to wallet.scanResponse.cardTypesResolver - } + this.wallets = wallets uiState = stateFactory.getSkeletonState(wallets = wallets) @@ -99,8 +99,15 @@ internal class WalletViewModel @Inject constructor( } private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { - // TODO: use GetTransactionsUseCase + val cardTypeResolver = getCardTypeResolver(index) + if (cardTypeResolver.isMultiwalletAllowed()) { + updateByTokensList(index, isRefreshing) + } else { + updateByTxHistory(index) + } + } + private fun updateByTokensList(index: Int, isRefreshing: Boolean) { getTokenListUseCase(userWalletId = uiState.walletsListConfig.wallets[index].id) .distinctUntilChanged() .onEach { tokenListEither -> @@ -113,26 +120,50 @@ internal class WalletViewModel @Inject constructor( } .flowOn(dispatchers.io) .launchIn(viewModelScope) - .saveIn(jobHolder = getTokensJobHolder) + .saveIn(tokensJobHolder) } - private fun updateNotifications(index: Int, tokenList: TokenList) { + private fun updateByTxHistory(index: Int) { + viewModelScope.launch(dispatchers.io) { + val wallet = getWallet(index) + val blockchain = wallet.scanResponse.cardTypesResolver.getBlockchain() + uiState = stateFactory.getLoadingTxHistoryState( + itemsCountEither = txHistoryItemsCountUseCase( + networkId = blockchain.id, + derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath, + ), + ) + + updateTxHistory(networkId = blockchain.id) + updateNotifications(index) + } + } + + private fun updateTxHistory(networkId: String) { + uiState = stateFactory.getLoadedTxHistoryState(txHistoryEither = txHistoryItemsUseCase(networkId)) + } + + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { notificationsListFactory.create( - cardTypesResolver = requireNotNull( - value = cardTypeResolverMap[uiState.walletsListConfig.wallets[index].id], - lazyMessage = { - "CardTypeResolverMap doesn't contain this id = ${uiState.walletsListConfig.wallets[index].id}" - }, - ), + cardTypesResolver = getCardTypeResolver(index = index), tokenList = tokenList, ) .distinctUntilChanged() .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } .flowOn(dispatchers.io) .launchIn(viewModelScope) - .saveIn(jobHolder = getNotificationJobHolder) + .saveIn(jobHolder = notificationsJobHolder) } + private fun getWallet(index: Int): UserWallet { + return requireNotNull( + value = wallets.getOrNull(index), + lazyMessage = { "WalletsList doesn't contain element with index = $index" }, + ) + } + + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver + override fun onBackClick() = router.popBackStack() override fun onScanCardClick() { From 68f5cef7b591538cef9ed50752ab37f07d17760b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Jul 2023 19:44:09 +0300 Subject: [PATCH 083/100] Updated on 2026-08-14 --- .../repository/DefaultTokensRepository.kt | 2 +- .../data/tokens/utils/CardTokensFactory.kt | 11 +- .../tokens/utils/ResponseTokensFactory.kt | 15 ++- .../data/tokens/utils/TokensOperations.kt | 122 +++++++++++------- .../tokens/utils/UserTokensResponseFactory.kt | 5 +- 5 files changed, 93 insertions(+), 62 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt index 1c83a87d60..f38aae9394 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -67,7 +67,7 @@ internal class DefaultTokensRepository( "Unable to find a user wallet with provided ID: $userWalletId" } } - require(!userWallet.isMultiCurrency) { + require(userWallet.isMultiCurrency) { "Multi currency wallet excepted, but single currency wallet was found: $userWalletId" } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt index d41f73f91b..b569162bd8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -6,7 +6,6 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.Network import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken import com.tangem.domain.tokens.model.Token as DomainToken @@ -49,11 +48,11 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { } return DomainToken( - id = getTokenId(sdkToken, blockchain), - networkId = Network.ID(blockchain.id), + id = getTokenId(blockchain, sdkToken), + networkId = getNetworkId(blockchain), name = sdkToken.name, symbol = sdkToken.symbol, - iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain), + iconUrl = getTokenIconUrl(blockchain, sdkToken), decimals = sdkToken.decimals, isCustom = false, contractAddress = sdkToken.contractAddress, @@ -69,10 +68,10 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { return DomainToken( id = getCoinId(blockchain), - networkId = Network.ID(blockchain.id), + networkId = getNetworkId(blockchain), name = blockchain.fullName, symbol = blockchain.currency, - iconUrl = getCoinIconId(blockchain), + iconUrl = getCoinIconUrl(blockchain), decimals = blockchain.decimals(), isCustom = false, contractAddress = null, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt index 5ca7fea388..081b7ffa64 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt @@ -2,11 +2,9 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Token import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken @@ -29,17 +27,22 @@ internal class ResponseTokensFactory(private val demoConfig: DemoConfig) { } val sdkToken = createSdkToken(token) + val (tokenId, iconUrl) = if (sdkToken == null) { + getCoinId(blockchain) to getCoinIconUrl(blockchain) + } else { + getTokenId(blockchain, sdkToken) to getTokenIconUrl(blockchain, sdkToken) + } return Token( - id = getTokenId(sdkToken, blockchain), - networkId = Network.ID(blockchain.id), + id = tokenId, + networkId = getNetworkId(blockchain), name = token.name, symbol = token.symbol, decimals = token.decimals, - iconUrl = getCoinOrTokenIconUrl(sdkToken, blockchain), + iconUrl = iconUrl, contractAddress = token.contractAddress, derivationPath = token.derivationPath, - isCustom = isCustomToken(token.id, token.derivationPath, card.derivationStyle, blockchain), + isCustom = isCustomToken(tokenId), ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 72938103a5..157df77386 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -1,59 +1,26 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.IconsUtil import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.Network import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.tokens.model.Token as DomainToken private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" private const val TOKEN_ICON_SIZE = "large" private const val TOKEN_ICON_EXT = "png" -internal fun isCustomToken( - tokenId: String?, - tokenDerivationPath: String?, - cardDerivationStyle: DerivationStyle?, - blockchain: Blockchain, -): Boolean { - if (tokenId == null) return true +private const val COIN_ID_PREFIX = "coin_" +private const val TOKEN_ID_PREFIX = "token_" +private const val CUSTOM_TOKEN_ID_PREFIX = "custom_token_" +private const val TOKEN_ID_DELIMITER = '#' - if (tokenDerivationPath == null || cardDerivationStyle == null) return false - - return tokenDerivationPath != blockchain.derivationPath(cardDerivationStyle)?.rawPath -} - -internal fun getTokenId(token: SdkToken?, blockchain: Blockchain): Token.ID { - val tokenId = token?.id - - return when { - token == null -> getCoinId(blockchain) - tokenId == null -> getCustomTokenId(token.contractAddress, blockchain) - else -> Token.ID(tokenId) - } -} - -internal fun getCoinId(blockchain: Blockchain): Token.ID { - val value = blockchain.toCoinId() - - return Token.ID(value) -} - -internal fun getCoinOrTokenIconUrl(token: SdkToken?, blockchain: Blockchain): String? { - val tokenId = token?.id - - return when { - token == null -> getCoinIconId(blockchain) - tokenId == null -> IconsUtil.getTokenIconUri(blockchain, token)?.toString() - else -> getTokenIconUrlFromDefaultHost(tokenId) - } -} - -internal fun getCoinIconId(blockchain: Blockchain): String { - return getTokenIconUrlFromDefaultHost(blockchain.toCoinId()) +internal fun isCustomToken(tokenId: DomainToken.ID): Boolean { + return tokenId.value.startsWith(CUSTOM_TOKEN_ID_PREFIX) } internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? { @@ -64,12 +31,75 @@ internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? { } } -private fun getCustomTokenId(contractAddress: String, blockchain: Blockchain): Token.ID { - val value = "custom_${blockchain.id}_$contractAddress" +internal fun getBlockchain(networkId: Network.ID): Blockchain { + return Blockchain.fromId(networkId.value) +} - return Token.ID(value) +internal fun getNetworkId(blockchain: Blockchain): Network.ID { + val value = blockchain.id + + return Network.ID(value) +} + +internal fun getCoinId(blockchain: Blockchain): DomainToken.ID { + return getTokenOrCoinId(blockchain, token = null) +} + +internal fun getTokenId(blockchain: Blockchain, token: SdkToken): DomainToken.ID { + return getTokenOrCoinId(blockchain, token) +} + +internal fun getResponseTokenId(token: DomainToken): String? { + return token.id.value.substringAfter(TOKEN_ID_DELIMITER) + .takeUnless { token.isCustom } +} + +internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { + val tokenId = token.id + + return if (tokenId == null) { + IconsUtil.getTokenIconUri(blockchain, token)?.toString() + } else { + getTokenIconUrlFromDefaultHost(tokenId) + } +} + +internal fun getCoinIconUrl(blockchain: Blockchain): String? { + val coinId = when (blockchain) { + Blockchain.Unknown -> null + Blockchain.TerraV1, Blockchain.TerraV2 -> blockchain.toCoinId() + else -> blockchain.toNetworkId() + } + + return coinId?.let(::getTokenIconUrlFromDefaultHost) +} + +private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): DomainToken.ID { + val sdkTokenId = token?.id + val (prefix, suffix) = when { + token == null -> COIN_ID_PREFIX to blockchain.toCoinId() + sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to token.contractAddress + else -> TOKEN_ID_PREFIX to sdkTokenId + } + + val value = buildString { + append(prefix) + append(blockchain.id) + append(TOKEN_ID_DELIMITER) + append(suffix.lowercase()) + } + + return DomainToken.ID(value) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { - return "$DEFAULT_TOKENS_ICONS_HOST/$TOKEN_ICON_SIZE/$tokenId.$TOKEN_ICON_EXT" + return buildString { + append(DEFAULT_TOKENS_ICONS_HOST) + append('/') + append(TOKEN_ICON_SIZE) + append('/') + append(tokenId) + append('.') + append(TOKEN_ICON_EXT) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index 721dc275f6..a7c85ba0d6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -1,6 +1,5 @@ package com.tangem.data.tokens.utils -import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.Token @@ -28,10 +27,10 @@ internal class UserTokensResponseFactory { } private fun createResponseToken(domainToken: Token): UserTokensResponse.Token { - val blockchain = Blockchain.fromId(domainToken.networkId.value) + val blockchain = getBlockchain(domainToken.networkId) return UserTokensResponse.Token( - id = domainToken.id.value.takeUnless { domainToken.isCustom }, + id = getResponseTokenId(domainToken), networkId = blockchain.toNetworkId(), derivationPath = domainToken.derivationPath, name = domainToken.name, From 642bcca31f83da9b34ad5b9a1a07fcd3c749b1fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Aug 2023 12:31:00 +0300 Subject: [PATCH 084/100] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b791bd4cf6..8c1c53b739 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b791bd4cf6c5eca9778f89e87cd62b72d24f5ce9 +Subproject commit 8c1c53b73950698d4acfa4d925b8c14bf6f0da63 From 81a16c8bb145717630e476b3a67c943e85021577 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Aug 2023 13:56:39 +0300 Subject: [PATCH 085/100] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 6 +- .../com/tangem/data/tokens/mock/MockQuotes.kt | 20 +- .../com/tangem/data/tokens/mock/MockTokens.kt | 48 ++-- .../repository/DefaultTokensRepository.kt | 34 +-- .../repository/MockNetworksRepository.kt | 4 +- .../tokens/repository/MockQuotesRepository.kt | 6 +- ...ensFactory.kt => CardCurrenciesFactory.kt} | 18 +- .../tokens/utils/ResponseCurrenciesFactory.kt | 77 ++++++ .../tokens/utils/ResponseTokensFactory.kt | 60 ----- .../data/tokens/utils/TokensOperations.kt | 18 +- .../tokens/utils/UserTokensResponseFactory.kt | 22 +- .../tokens/ApplyTokenListSortingUseCase.kt | 20 +- ...seCase.kt => GetPrimaryCurrencyUseCase.kt} | 22 +- .../domain/tokens/GetTokenListUseCase.kt | 14 +- .../tokens/ToggleTokenListGroupingUseCase.kt | 4 +- .../tokens/ToggleTokenListSortingUseCase.kt | 2 +- .../mapper/GetWalletTokenErrorMappers.kt | 12 +- .../error/mapper/TokenListErrorMappers.kt | 12 +- .../domain/tokens/model/CryptoCurrency.kt | 61 +++++ ...TokenStatus.kt => CryptoCurrencyStatus.kt} | 10 +- .../domain/tokens/model/NetworkGroup.kt | 2 +- .../domain/tokens/model/NetworkStatus.kt | 6 +- .../com/tangem/domain/tokens/model/Quote.kt | 2 +- .../com/tangem/domain/tokens/model/Token.kt | 31 --- .../tangem/domain/tokens/model/TokenList.kt | 2 +- ...ons.kt => CurrenciesStatusesOperations.kt} | 57 +++-- ...rations.kt => CurrencyStatusOperations.kt} | 40 ++-- .../TokenListFiatBalanceOperations.kt | 22 +- .../tokens/operations/TokenListOperations.kt | 24 +- .../operations/TokenListSortingOperations.kt | 30 +-- .../tokens/repository/NetworksRepository.kt | 4 +- .../tokens/repository/QuotesRepository.kt | 4 +- .../tokens/repository/TokensRepository.kt | 8 +- .../ApplyTokenListSortingUseCaseTest.kt | 4 +- .../domain/tokens/GetTokenListUseCaseTest.kt | 6 +- .../domain/tokens/mock/MockNetworksGroups.kt | 20 +- .../tangem/domain/tokens/mock/MockQuotes.kt | 20 +- .../domain/tokens/mock/MockTokenLists.kt | 18 +- .../tangem/domain/tokens/mock/MockTokens.kt | 226 +++++++++--------- .../domain/tokens/mock/MockTokensStates.kt | 135 +++-------- .../repository/MockNetworksRepository.kt | 4 +- .../tokens/repository/MockQuotesRepository.kt | 4 +- .../tokens/repository/MockTokensRepository.kt | 17 +- ...yptoCurrencyStatusToTokenItemConverter.kt} | 51 ++-- .../utils/TokenListToContentItemsConverter.kt | 10 +- .../WalletNotificationsListFactory.kt | 8 +- 46 files changed, 610 insertions(+), 615 deletions(-) rename data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/{CardTokensFactory.kt => CardCurrenciesFactory.kt} (84%) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetTokenUseCase.kt => GetPrimaryCurrencyUseCase.kt} (70%) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/{TokenStatus.kt => CryptoCurrencyStatus.kt} (84%) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/{TokensStatusesOperations.kt => CurrenciesStatusesOperations.kt} (69%) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/{TokenStatusOperations.kt => CurrencyStatusOperations.kt} (64%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/{TokenStatusToTokenItemConverter.kt => CryptoCurrencyStatusToTokenItemConverter.kt} (63%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ddf863762d..8fc08144df 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -28,13 +28,13 @@ internal object TokensDomainModule { @Provides @ViewModelScoped - fun provideGetTokenUseCase( + fun provideGetPrimaryCurrencyUseCase( tokensRepository: TokensRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetTokenUseCase { - return GetTokenUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + ): GetPrimaryCurrencyUseCase { + return GetPrimaryCurrencyUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt index 86f7a6d76c..f89c208a52 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt @@ -7,61 +7,61 @@ import java.math.BigDecimal internal object MockQuotes { val quote1 = Quote( - tokenId = MockTokens.token1.id, + currencyId = MockTokens.token1.id, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), ) val quote2 = Quote( - tokenId = MockTokens.token2.id, + currencyId = MockTokens.token2.id, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), ) val quote3 = Quote( - tokenId = MockTokens.token3.id, + currencyId = MockTokens.token3.id, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), ) val quote4 = Quote( - tokenId = MockTokens.token4.id, + currencyId = MockTokens.token4.id, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), ) val quote5 = Quote( - tokenId = MockTokens.token5.id, + currencyId = MockTokens.token5.id, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), ) val quote6 = Quote( - tokenId = MockTokens.token6.id, + currencyId = MockTokens.token6.id, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), ) val quote7 = Quote( - tokenId = MockTokens.token7.id, + currencyId = MockTokens.token7.id, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), ) val quote8 = Quote( - tokenId = MockTokens.token8.id, + currencyId = MockTokens.token8.id, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), ) val quote9 = Quote( - tokenId = MockTokens.token9.id, + currencyId = MockTokens.token9.id, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), ) val quote10 = Quote( - tokenId = MockTokens.token10.id, + currencyId = MockTokens.token10.id, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt index 97384de272..e18e4c126d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -1,25 +1,23 @@ package com.tangem.data.tokens.mock -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId internal object MockTokens { val token1 - get() = Token( - id = Token.ID("token1"), + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token1"), networkId = MockNetworks.network1.id, name = "Token 1", symbol = "T1", - isCustom = false, decimals = 8, iconUrl = null, - contractAddress = null, derivationPath = null, ) val token2 - get() = Token( - id = Token.ID("token2"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token2"), networkId = MockNetworks.network1.id, name = "Token 2", symbol = "T2", @@ -30,8 +28,8 @@ internal object MockTokens { derivationPath = null, ) val token3 - get() = Token( - id = Token.ID("token3"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token3"), networkId = MockNetworks.network1.id, name = "Token 3", symbol = "T3", @@ -42,20 +40,18 @@ internal object MockTokens { derivationPath = null, ) val token4 - get() = Token( - id = Token.ID("token4"), + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token4"), networkId = MockNetworks.network2.id, name = "Token 4", symbol = "T4", - isCustom = false, decimals = 8, iconUrl = null, - contractAddress = null, derivationPath = null, ) val token5 - get() = Token( - id = Token.ID("token5"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token5"), networkId = MockNetworks.network2.id, name = "Token 5", symbol = "T5", @@ -66,8 +62,8 @@ internal object MockTokens { derivationPath = null, ) val token6 - get() = Token( - id = Token.ID("token6"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token6"), networkId = MockNetworks.network2.id, name = "Token 6", symbol = "T6", @@ -78,20 +74,18 @@ internal object MockTokens { derivationPath = null, ) val token7 - get() = Token( - id = Token.ID("token7"), + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token7"), networkId = MockNetworks.network3.id, name = "Token 7", symbol = "T7", - isCustom = false, decimals = 8, iconUrl = null, - contractAddress = null, derivationPath = null, ) val token8 - get() = Token( - id = Token.ID("token8"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token8"), networkId = MockNetworks.network3.id, name = "Token 8", symbol = "T8", @@ -102,8 +96,8 @@ internal object MockTokens { derivationPath = null, ) val token9 - get() = Token( - id = Token.ID("token9"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token9"), networkId = MockNetworks.network3.id, name = "Token 9", symbol = "T9", @@ -114,8 +108,8 @@ internal object MockTokens { derivationPath = null, ) val token10 - get() = Token( - id = Token.ID("token10"), + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token10"), networkId = MockNetworks.network3.id, name = "Token 10", symbol = "T10", diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt index f38aae9394..704e25fa68 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -1,15 +1,15 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.CardTokensFactory -import com.tangem.data.tokens.utils.ResponseTokensFactory +import com.tangem.data.tokens.utils.CardCurrenciesFactory +import com.tangem.data.tokens.utils.ResponseCurrenciesFactory import com.tangem.data.tokens.utils.UserTokensResponseFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -27,18 +27,18 @@ internal class DefaultTokensRepository( ) : TokensRepository { private val demoConfig = DemoConfig() - private val responseTokensFactory = ResponseTokensFactory(demoConfig) - private val cardTokensFactory = CardTokensFactory(demoConfig) + private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig) + private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig) private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun saveTokens( userWalletId: UserWalletId, - tokens: Set, + currencies: Set, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { val response = userTokensResponseFactory.createUserTokensResponse( - tokens = tokens, + currencies = currencies, isGroupedByNetwork = isGroupedByNetwork, isSortedByBalance = isSortedByBalance, ) @@ -46,7 +46,7 @@ internal class DefaultTokensRepository( storeAndPushTokens(userWalletId, response) } - override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { + override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { val userWallet = withContext(dispatchers.io) { requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" @@ -56,11 +56,13 @@ internal class DefaultTokensRepository( "Single currency wallet excepted, but multi currency wallet was found: $userWalletId" } - return cardTokensFactory - .createPrimaryTokenForSingleCurrencyCard(userWallet.scanResponse) + return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) } - override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + override fun getMultiCurrencyWalletCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { return channelFlow { val userWallet = withContext(dispatchers.io) { requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { @@ -72,7 +74,7 @@ internal class DefaultTokensRepository( } launch(dispatchers.io) { - getMultiCurrencyWalletTokens(userWallet).collectLatest(::send) + getMultiCurrencyWalletCurrencies(userWallet).collectLatest(::send) } launch(dispatchers.io) { @@ -93,9 +95,9 @@ internal class DefaultTokensRepository( .flowOn(dispatchers.io) } - private fun getMultiCurrencyWalletTokens(userWallet: UserWallet): Flow> { + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> - responseTokensFactory.createTokens( + responseCurrenciesFactory.createTokens( response = storedTokens, card = userWallet.scanResponse.card, ) @@ -131,7 +133,9 @@ internal class DefaultTokensRepository( if (NOT_FOUND_HTTP_CODE in errorMessage) { val response = userTokensStore.getSyncOrNull(userWallet.walletId) ?: userTokensResponseFactory.createUserTokensResponse( - tokens = cardTokensFactory.createDefaultTokensForMultiCurrencyCard(userWallet.scanResponse.card), + currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard( + userWallet.scanResponse.card, + ), isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt index 26bc58aa70..c6919ea86b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt @@ -1,9 +1,9 @@ package com.tangem.data.tokens.repository import com.tangem.data.tokens.mock.MockNetworks +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -19,7 +19,7 @@ internal class MockNetworksRepository : NetworksRepository { override fun getNetworkStatuses( userWalletId: UserWalletId, - networks: Map>, + networks: Map>, refresh: Boolean, ): Flow> { return flowOf( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt index 2510893558..345f17e6bc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt @@ -1,18 +1,18 @@ package com.tangem.data.tokens.repository import com.tangem.data.tokens.mock.MockQuotes +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.QuotesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf internal class MockQuotesRepository : QuotesRepository { - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { return flowOf( MockQuotes.quotes - .filter { it.tokenId in tokensIds } + .filter { it.currencyId in tokensIds } .toSet(), ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt similarity index 84% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt index b569162bd8..15fd96b0aa 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt @@ -6,13 +6,13 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken -import com.tangem.domain.tokens.model.Token as DomainToken -internal class CardTokensFactory(private val demoConfig: DemoConfig) { +internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { - fun createDefaultTokensForMultiCurrencyCard(card: CardDTO): Set { + fun createDefaultCoinsForMultiCurrencyCard(card: CardDTO): Set { var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { demoConfig.demoBlockchains } else { @@ -26,7 +26,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { return blockchains.mapNotNull { createCoin(it, card) }.toSet() } - fun createPrimaryTokenForSingleCurrencyCard(scanResponse: ScanResponse): DomainToken { + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { val card = scanResponse.card val resolver = scanResponse.cardTypesResolver val blockchain = resolver.getBlockchain() @@ -41,13 +41,13 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { return primaryToken ?: coin } - private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? { + private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): CryptoCurrency.Token? { if (blockchain != Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } - return DomainToken( + return CryptoCurrency.Token( id = getTokenId(blockchain, sdkToken), networkId = getNetworkId(blockchain), name = sdkToken.name, @@ -60,21 +60,19 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { ) } - private fun createCoin(blockchain: Blockchain, card: CardDTO): DomainToken? { + private fun createCoin(blockchain: Blockchain, card: CardDTO): CryptoCurrency.Coin? { if (blockchain != Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } - return DomainToken( + return CryptoCurrency.Coin( id = getCoinId(blockchain), networkId = getNetworkId(blockchain), name = blockchain.fullName, symbol = blockchain.currency, iconUrl = getCoinIconUrl(blockchain), decimals = blockchain.decimals(), - isCustom = false, - contractAddress = null, derivationPath = getDerivationPath(blockchain, card), ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt new file mode 100644 index 0000000000..0a6c78b0e5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt @@ -0,0 +1,77 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken + +internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { + + fun createTokens(response: UserTokensResponse, card: CardDTO): Set { + return response.tokens.mapNotNull { createToken(it, card) }.toSet() + } + + private fun createToken(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + var blockchain = Blockchain.fromNetworkId(responseToken.networkId) + if (blockchain == null || blockchain == Blockchain.Unknown) { + Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + return null + } + + if (demoConfig.isDemoCardId(card.cardId)) { + blockchain = blockchain.getTestnetVersion() ?: blockchain + } + + val sdkToken = createSdkToken(responseToken) + return if (sdkToken == null) { + createCoin(blockchain, responseToken) + } else { + createToken(blockchain, sdkToken, responseToken.derivationPath) + } + } + + private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? { + return token.contractAddress?.let { contractAddress -> + SdkToken( + name = token.name, + symbol = token.symbol, + contractAddress = contractAddress, + decimals = token.decimals, + id = token.id, + ) + } + } + + private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin { + return CryptoCurrency.Coin( + id = getCoinId(blockchain), + networkId = getNetworkId(blockchain), + name = responseToken.name, + symbol = responseToken.symbol, + decimals = responseToken.decimals, + derivationPath = responseToken.derivationPath, + iconUrl = getCoinIconUrl(blockchain), + ) + } + + private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token { + val id = getTokenId(blockchain, sdkToken) + + return CryptoCurrency.Token( + id = id, + networkId = getNetworkId(blockchain), + name = sdkToken.name, + symbol = sdkToken.symbol, + decimals = sdkToken.decimals, + derivationPath = derivationPath, + iconUrl = getTokenIconUrl(blockchain, sdkToken), + contractAddress = sdkToken.contractAddress, + isCustom = isCustomToken(id), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt deleted file mode 100644 index 081b7ffa64..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.model.Token -import timber.log.Timber -import com.tangem.blockchain.common.Token as SdkToken - -internal class ResponseTokensFactory(private val demoConfig: DemoConfig) { - - fun createTokens(response: UserTokensResponse, card: CardDTO): Set { - return response.tokens.mapNotNull { createToken(it, card) }.toSet() - } - - private fun createToken(token: UserTokensResponse.Token, card: CardDTO): Token? { - var blockchain = Blockchain.fromNetworkId(token.networkId) - if (blockchain == null) { - Timber.e("Unable to find a blockchain with the network ID: ${token.networkId}") - return null - } - - if (demoConfig.isDemoCardId(card.cardId)) { - blockchain = blockchain.getTestnetVersion() ?: blockchain - } - - val sdkToken = createSdkToken(token) - val (tokenId, iconUrl) = if (sdkToken == null) { - getCoinId(blockchain) to getCoinIconUrl(blockchain) - } else { - getTokenId(blockchain, sdkToken) to getTokenIconUrl(blockchain, sdkToken) - } - - return Token( - id = tokenId, - networkId = getNetworkId(blockchain), - name = token.name, - symbol = token.symbol, - decimals = token.decimals, - iconUrl = iconUrl, - contractAddress = token.contractAddress, - derivationPath = token.derivationPath, - isCustom = isCustomToken(tokenId), - ) - } - - private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? { - return token.contractAddress?.let { contractAddress -> - SdkToken( - name = token.name, - symbol = token.symbol, - contractAddress = contractAddress, - decimals = token.decimals, - id = token.id, - ) - } - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 157df77386..627f30111d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -6,9 +6,9 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.blockchain.common.Token as SdkToken -import com.tangem.domain.tokens.model.Token as DomainToken private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" private const val TOKEN_ICON_SIZE = "large" @@ -19,7 +19,7 @@ private const val TOKEN_ID_PREFIX = "token_" private const val CUSTOM_TOKEN_ID_PREFIX = "custom_token_" private const val TOKEN_ID_DELIMITER = '#' -internal fun isCustomToken(tokenId: DomainToken.ID): Boolean { +internal fun isCustomToken(tokenId: CryptoCurrency.ID): Boolean { return tokenId.value.startsWith(CUSTOM_TOKEN_ID_PREFIX) } @@ -41,17 +41,17 @@ internal fun getNetworkId(blockchain: Blockchain): Network.ID { return Network.ID(value) } -internal fun getCoinId(blockchain: Blockchain): DomainToken.ID { +internal fun getCoinId(blockchain: Blockchain): CryptoCurrency.ID { return getTokenOrCoinId(blockchain, token = null) } -internal fun getTokenId(blockchain: Blockchain, token: SdkToken): DomainToken.ID { +internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency.ID { return getTokenOrCoinId(blockchain, token) } -internal fun getResponseTokenId(token: DomainToken): String? { - return token.id.value.substringAfter(TOKEN_ID_DELIMITER) - .takeUnless { token.isCustom } +internal fun getResponseTokenId(currency: CryptoCurrency): String? { + return currency.id.value.substringAfter(TOKEN_ID_DELIMITER) + .takeUnless { currency is CryptoCurrency.Token && currency.isCustom } } internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { @@ -74,7 +74,7 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): DomainToken.ID { +private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): CryptoCurrency.ID { val sdkTokenId = token?.id val (prefix, suffix) = when { token == null -> COIN_ID_PREFIX to blockchain.toCoinId() @@ -89,7 +89,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): DomainTo append(suffix.lowercase()) } - return DomainToken.ID(value) + return CryptoCurrency.ID(value) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index a7c85ba0d6..ffb4fc9c10 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -2,17 +2,17 @@ package com.tangem.data.tokens.utils import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency internal class UserTokensResponseFactory { fun createUserTokensResponse( - tokens: Set, + currencies: Set, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): UserTokensResponse { return UserTokensResponse( - tokens = tokens.map(::createResponseToken), + tokens = currencies.map(::createResponseToken), group = if (isGroupedByNetwork) { UserTokensResponse.GroupType.NETWORK } else { @@ -26,17 +26,17 @@ internal class UserTokensResponseFactory { ) } - private fun createResponseToken(domainToken: Token): UserTokensResponse.Token { - val blockchain = getBlockchain(domainToken.networkId) + private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token { + val blockchain = getBlockchain(currency.networkId) return UserTokensResponse.Token( - id = getResponseTokenId(domainToken), + id = getResponseTokenId(currency), networkId = blockchain.toNetworkId(), - derivationPath = domainToken.derivationPath, - name = domainToken.name, - symbol = domainToken.symbol, - decimals = domainToken.decimals, - contractAddress = domainToken.contractAddress, + derivationPath = currency.derivationPath, + name = currency.name, + symbol = currency.symbol, + decimals = currency.decimals, + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 2004da059c..c1bad61702 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -7,8 +7,8 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -22,7 +22,7 @@ class ApplyTokenListSortingUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - sortedTokensIds: Set>, + sortedTokensIds: Set>, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): Either { @@ -30,7 +30,7 @@ class ApplyTokenListSortingUseCase( either { applySorting( userWalletId = userWalletId, - tokens = sortTokens(sortedTokensIds, getTokens(userWalletId)), + tokens = sortTokens(sortedTokensIds, getCurrencies(userWalletId)), isGrouped = isGroupedByNetwork, isSortedByBalance = isSortedByBalance, ) @@ -39,14 +39,14 @@ class ApplyTokenListSortingUseCase( } private suspend fun Raise.sortTokens( - sortedTokensIds: Set>, - unsortedTokens: Set, - ): Set = withContext(dispatchers.default) { + sortedTokensIds: Set>, + unsortedTokens: Set, + ): Set = withContext(dispatchers.default) { val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) { TokenListSortingError.TokenListIsEmpty } - val sortedTokens = sortedMapOf() + val sortedTokens = sortedMapOf() unsortedTokens.forEach { token -> val index = nonEmptySortedTokensIds.indexOfFirst { (networkId, tokenId) -> @@ -65,9 +65,9 @@ class ApplyTokenListSortingUseCase( } } - private suspend fun Raise.getTokens(userWalletId: UserWalletId): Set { + private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): Set { val tokens = catch( - block = { tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh = false).firstOrNull() }, + block = { tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() }, catch = { raise(TokenListSortingError.DataError(it)) }, ) @@ -78,7 +78,7 @@ class ApplyTokenListSortingUseCase( private suspend fun Raise.applySorting( userWalletId: UserWalletId, - tokens: Set, + tokens: Set, isGrouped: Boolean, isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt similarity index 70% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt index fd3ea69f0b..bfc25b58da 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt @@ -7,8 +7,8 @@ import arrow.core.raise.recover import arrow.core.right import com.tangem.domain.tokens.error.TokenError import com.tangem.domain.tokens.error.mapper.mapToTokenError -import com.tangem.domain.tokens.model.TokenStatus -import com.tangem.domain.tokens.operations.TokensStatusesOperations +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository @@ -18,14 +18,17 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest -class GetTokenUseCase( +class GetPrimaryCurrencyUseCase( private val tokensRepository: TokensRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow> { + operator fun invoke( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): Flow> { return channelFlow { recover( block = { @@ -40,8 +43,11 @@ class GetTokenUseCase( } } - private suspend fun Raise.getToken(userWalletId: UserWalletId, refresh: Boolean): Flow { - val operations = TokensStatusesOperations( + private suspend fun Raise.getToken( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow { + val operations = CurrenciesStatusesOperations( tokensRepository = tokensRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, @@ -49,9 +55,9 @@ class GetTokenUseCase( refresh = refresh, dispatchers = dispatchers, raise = this, - transformError = TokensStatusesOperations.Error::mapToTokenError, + transformError = CurrenciesStatusesOperations.Error::mapToTokenError, ) - return operations.getSingleCurrencyWalletTokenStatusFlow() + return operations.getPrimaryCurrencyStatusFlow() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 9f07999ccd..0a142e1241 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -7,10 +7,10 @@ import arrow.core.raise.recover import arrow.core.right import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.operations.TokensStatusesOperations import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository @@ -51,21 +51,21 @@ class GetTokenListUseCase( private fun Raise.getTokensStatuses( userWalletId: UserWalletId, refresh: Boolean, - ): Flow> { - val operations = TokensStatusesOperations( + ): Flow> { + val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, refresh = refresh, useCase = this@GetTokenListUseCase, raise = this, - transformError = TokensStatusesOperations.Error::mapToTokenListError, + transformError = CurrenciesStatusesOperations.Error::mapToTokenListError, ) - return operations.getMultiCurrencyWalletTokensStatusesFlow() + return operations.getMultiCurrencyWalletStatusesFlow() } private fun Raise.createTokenList( userWalletId: UserWalletId, - tokens: Set, + tokens: Set, ): Flow { val operations = TokenListOperations( userWalletId = userWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index 4d65f82422..cb53f81ec2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -41,7 +41,7 @@ class ToggleTokenListGroupingUseCase( val sortingOperations = getSortingOperations(tokenList) val tokens = sortingOperations.getTokens() - val networks = getNetworks(tokens.map { it.networkId }.toSet()) + val networks = getNetworks(tokens.map { it.currency.networkId }.toSet()) return TokenList.GroupedByNetwork( groups = sortingOperations.getGroupedTokens(networks), totalFiatBalance = tokenList.totalFiatBalance, @@ -55,7 +55,7 @@ class ToggleTokenListGroupingUseCase( val sortingOperations = getSortingOperations(tokenList) return TokenList.Ungrouped( - tokens = sortingOperations.getTokens(), + currencies = sortingOperations.getTokens(), totalFiatBalance = tokenList.totalFiatBalance, sortedBy = sortingOperations.getSortType(), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index 4b0b8664f4..b91ce9521a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -49,7 +49,7 @@ class ToggleTokenListSortingUseCase( val operations = getSortingOperations(tokenList) return tokenList.copy( - tokens = operations.getTokens(), + currencies = operations.getTokens(), sortedBy = operations.getSortType(), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt index 25f2bf46d3..1252251389 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -1,14 +1,14 @@ package com.tangem.domain.tokens.error.mapper import com.tangem.domain.tokens.error.TokenError -import com.tangem.domain.tokens.operations.TokensStatusesOperations +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -internal fun TokensStatusesOperations.Error.mapToTokenError(): TokenError { +internal fun CurrenciesStatusesOperations.Error.mapToTokenError(): TokenError { return when (this) { - is TokensStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) - is TokensStatusesOperations.Error.EmptyNetworksStatuses, - is TokensStatusesOperations.Error.EmptyQuotes, - is TokensStatusesOperations.Error.EmptyTokens, + is CurrenciesStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, + is CurrenciesStatusesOperations.Error.EmptyQuotes, + is CurrenciesStatusesOperations.Error.EmptyCurrencies, -> TokenError.UnableToCreateToken } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 2d0798a489..9b24b82737 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -1,15 +1,15 @@ package com.tangem.domain.tokens.error.mapper import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.operations.TokensStatusesOperations -internal fun TokensStatusesOperations.Error.mapToTokenListError(): TokenListError { +internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenListError { return when (this) { - is TokensStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause) - is TokensStatusesOperations.Error.EmptyNetworksStatuses, - is TokensStatusesOperations.Error.EmptyQuotes, - is TokensStatusesOperations.Error.EmptyTokens, + is CurrenciesStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, + is CurrenciesStatusesOperations.Error.EmptyQuotes, + is CurrenciesStatusesOperations.Error.EmptyCurrencies, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt new file mode 100644 index 0000000000..891d75986e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.tokens.model + +sealed class CryptoCurrency { + + abstract val id: ID + abstract val networkId: Network.ID + abstract val name: String + abstract val symbol: String + abstract val decimals: Int + abstract val iconUrl: String? + abstract val derivationPath: String? + + data class Coin( + override val id: ID, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + ) : CryptoCurrency() { + + init { + checkProperties() + } + } + + data class Token( + override val id: ID, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + val contractAddress: String, + val isCustom: Boolean, + ) : CryptoCurrency() { + + init { + checkProperties() + require(contractAddress.isNotBlank()) { "Token contract address must not be blank" } + } + } + + @JvmInline + value class ID(val value: String) { + + init { + require(value.isNotBlank()) { "Crypto currency ID must not be blank" } + } + } + + protected fun checkProperties() { + require(name.isNotBlank()) { "Crypto currency name must not be blank" } + require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } + require(iconUrl?.isNotBlank() ?: true) { "Crypto currency icon URL must not be blank" } + require(decimals > 0) { "Crypto currency decimal must not be less then zero, but it is: $decimals" } + require(derivationPath?.isNotBlank() ?: true) { "Crypto currency derivation path must not be blank" } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt similarity index 84% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 9a217c2f35..f67fc23e5a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -2,14 +2,8 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal -data class TokenStatus( - val id: Token.ID, - val networkId: Network.ID, - val name: String, - val symbol: String, - val decimals: Int, - val iconUrl: String?, - val isCoin: Boolean, +data class CryptoCurrencyStatus( + val currency: CryptoCurrency, val value: Status, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index d1cc917d38..51d03a899e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -2,5 +2,5 @@ package com.tangem.domain.tokens.model data class NetworkGroup( val network: Network, - val tokens: Set, + val currencies: Set, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index 36d1bc30e6..f76108f535 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -8,16 +8,16 @@ data class NetworkStatus( ) { sealed class Status { - open val amounts: Map? = null + open val amounts: Map? = null } object Unreachable : Status() object MissedDerivation : Status() - data class TransactionInProgress(override val amounts: Map) : Status() + data class TransactionInProgress(override val amounts: Map) : Status() - data class Verified(override val amounts: Map) : Status() + data class Verified(override val amounts: Map) : Status() data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt index fad3db3666..192c196537 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt @@ -3,7 +3,7 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal data class Quote( - val tokenId: Token.ID, + val currencyId: CryptoCurrency.ID, val fiatRate: BigDecimal, val priceChange: BigDecimal, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt deleted file mode 100644 index 745acaa6b5..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.domain.tokens.model - -data class Token( - val id: ID, - val networkId: Network.ID, - val name: String, - val symbol: String, - val iconUrl: String?, - val decimals: Int, - val isCustom: Boolean, - val contractAddress: String?, - val derivationPath: String?, -) { - - init { - require(name.isNotBlank()) { "Token name must not be blank" } - require(symbol.isNotBlank()) { "Token symbol must not be blank" } - require(iconUrl?.isNotBlank() ?: true) { "Token icon URL must not be blank" } - require(decimals > 0) { "Token decimal must not be less then zero, but it is: $decimals" } - require(contractAddress?.isNotBlank() ?: true) { "Token contract address must not be blank" } - require(derivationPath?.isNotBlank() ?: true) { "Token derivation path must not be blank" } - } - - @JvmInline - value class ID(val value: String) { - - init { - require(value.isNotBlank()) { "Token ID value must not be blank" } - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 3f1ffaaf75..f19f35bef2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -13,7 +13,7 @@ sealed class TokenList { ) : TokenList() data class Ungrouped( - val tokens: Set, + val currencies: Set, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt similarity index 69% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 7b7eced70a..e64237f1a3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext @Suppress("LongParameterList") -internal class TokensStatusesOperations( +internal class CurrenciesStatusesOperations( private val tokensRepository: TokensRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, @@ -24,7 +24,7 @@ internal class TokensStatusesOperations( private val dispatchers: CoroutineDispatcherProvider, raise: Raise, transformError: (Error) -> E, -) : DelegatedRaise(raise, transformError) { +) : DelegatedRaise(raise, transformError) { constructor( userWalletId: UserWalletId, @@ -43,8 +43,8 @@ internal class TokensStatusesOperations( transformError = transformError, ) - fun getMultiCurrencyWalletTokensStatusesFlow(): Flow> { - return getMultiCurrencyWalletTokens().flatMapConcat { + fun getMultiCurrencyWalletStatusesFlow(): Flow> { + return getMultiCurrencyWalletCurrencies().flatMapConcat { val tokens = it.toNonEmptySetOrNull() if (tokens == null) { @@ -60,12 +60,12 @@ internal class TokensStatusesOperations( } } - suspend fun getSingleCurrencyWalletTokenStatusFlow(): Flow { - val token = getSingleCurrencyWalletToken() + suspend fun getPrimaryCurrencyStatusFlow(): Flow { + val token = getPrimaryCurrency() val quoteFlow = getQuotes(nonEmptySetOf(token.id)) .map { quotes -> - quotes.singleOrNull { it.tokenId == token.id } + quotes.singleOrNull { it.currencyId == token.id } } val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token))) @@ -79,61 +79,69 @@ internal class TokensStatusesOperations( } private suspend fun createTokensStatuses( - tokens: Set, + tokens: Set, quotes: Set, networkStatuses: Set, - ): Set = withContext(dispatchers.default) { + ): Set = withContext(dispatchers.default) { tokens.mapTo(hashSetOf()) { token -> - val quote = quotes.firstOrNull { it.tokenId == token.id } + val quote = quotes.firstOrNull { it.currencyId == token.id } val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } createStatus(token, quote, networkStatus) } } - private suspend fun createStatus(token: Token, quote: Quote?, networkStatus: NetworkStatus?): TokenStatus { - val tokenStatusOperations = TokenStatusOperations( - token = token, + private suspend fun createStatus( + token: CryptoCurrency, + quote: Quote?, + networkStatus: NetworkStatus?, + ): CryptoCurrencyStatus { + val currencyStatusOperations = CurrencyStatusOperations( + currency = token, quote = quote, networkStatus = networkStatus, dispatchers = dispatchers, ) - return tokenStatusOperations.createTokenStatus() + return currencyStatusOperations.createTokenStatus() } - private fun getMultiCurrencyWalletTokens(): Flow> { - return tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh) + private fun getMultiCurrencyWalletCurrencies(): Flow> { + return tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh) .catch { raise(Error.DataError(it)) } - .onEmpty { raise(Error.EmptyTokens) } + .onEmpty { raise(Error.EmptyCurrencies) } .flowOn(dispatchers.io) } - private suspend fun getSingleCurrencyWalletToken(): Token { + private suspend fun getPrimaryCurrency(): CryptoCurrency { return withContext(dispatchers.io) { catch( - block = { tokensRepository.getSingleCurrencyWalletToken(userWalletId) }, + block = { tokensRepository.getPrimaryCurrency(userWalletId) }, catch = { raise(Error.DataError(it)) }, ) } } - private fun getQuotes(tokensIds: NonEmptySet): Flow> { + private fun getQuotes(tokensIds: NonEmptySet): Flow> { return quotesRepository.getQuotes(tokensIds, refresh) .catch { raise(Error.DataError(it)) } .onEmpty { raise(Error.EmptyQuotes) } .flowOn(dispatchers.io) } - private fun getNetworksStatues(groupedTokens: Map>): Flow> { + private fun getNetworksStatues( + groupedTokens: Map>, + ): Flow> { return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) .catch { raise(Error.DataError(it)) } .onEmpty { raise(Error.EmptyNetworksStatuses) } .flowOn(dispatchers.io) } - private suspend fun groupTokens(tokens: NonEmptySet): Map> = - withContext(dispatchers.default) { + private suspend fun groupTokens( + tokens: NonEmptySet, + ): Map> { + return withContext(dispatchers.default) { tokens .groupBy { it.networkId } .mapValues { (_, tokens) -> @@ -143,10 +151,11 @@ internal class TokensStatusesOperations( .toNonEmptySet() } } + } sealed class Error { - object EmptyTokens : Error() + object EmptyCurrencies : Error() object EmptyQuotes : Error() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt similarity index 64% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 250ba9932f..a48964936b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -1,39 +1,33 @@ package com.tangem.domain.tokens.operations +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal -internal class TokenStatusOperations( - private val token: Token, +internal class CurrencyStatusOperations( + private val currency: CryptoCurrency, private val quote: Quote?, private val networkStatus: NetworkStatus?, private val dispatchers: CoroutineDispatcherProvider, ) { - suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.default) { - TokenStatus( - id = token.id, - networkId = token.networkId, - name = token.name, - symbol = token.symbol, - isCoin = token.contractAddress == null, - decimals = token.decimals, - iconUrl = token.iconUrl, + suspend fun createTokenStatus(): CryptoCurrencyStatus = withContext(dispatchers.default) { + CryptoCurrencyStatus( + currency = currency, value = createStatus(), ) } - private fun createStatus(): TokenStatus.Status { + private fun createStatus(): CryptoCurrencyStatus.Status { return when (val status = networkStatus?.value) { - null -> TokenStatus.Loading - is NetworkStatus.MissedDerivation -> TokenStatus.MissedDerivation - is NetworkStatus.Unreachable -> TokenStatus.Unreachable - is NetworkStatus.NoAccount -> TokenStatus.NoAccount + null -> CryptoCurrencyStatus.Loading + is NetworkStatus.MissedDerivation -> CryptoCurrencyStatus.MissedDerivation + is NetworkStatus.Unreachable -> CryptoCurrencyStatus.Unreachable + is NetworkStatus.NoAccount -> CryptoCurrencyStatus.NoAccount is NetworkStatus.TransactionInProgress, is NetworkStatus.Verified, -> createStatus( @@ -43,17 +37,17 @@ internal class TokenStatusOperations( } } - private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): TokenStatus.Status { + private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): CryptoCurrencyStatus.Status { return when { - token.isCustom -> TokenStatus.Custom( + currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, hasTransactionsInProgress = hasTransactionsInProgress, ) - quote == null -> TokenStatus.Loading - else -> TokenStatus.Loaded( + quote == null -> CryptoCurrencyStatus.Loading + else -> CryptoCurrencyStatus.Loaded( amount = amount, fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, @@ -64,7 +58,7 @@ internal class TokenStatusOperations( } private fun getTokenAmount(): BigDecimal { - val amount = networkStatus?.value?.amounts?.get(token.id) + val amount = networkStatus?.value?.amounts?.get(currency.id) return amount ?: error("Incorrect network status: $networkStatus") } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 69378510f3..dd3cb367d5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -1,14 +1,14 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptySet +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal internal class TokenListFiatBalanceOperations( - private val tokens: NonEmptySet, + private val currencies: NonEmptySet, private val isAnyTokenLoading: Boolean, private val dispatcher: CoroutineDispatcherProvider, ) { @@ -18,25 +18,25 @@ internal class TokenListFiatBalanceOperations( var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading if (isAnyTokenLoading) return@withContext fiatBalance - for (token in tokens) { + for (token in currencies) { when (val status = token.value) { - is TokenStatus.Loading -> { + is CryptoCurrencyStatus.Loading -> { fiatBalance = TokenList.FiatBalance.Loading break } - is TokenStatus.MissedDerivation, - is TokenStatus.Unreachable, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, -> { fiatBalance = TokenList.FiatBalance.Failed break } - is TokenStatus.NoAccount -> { + is CryptoCurrencyStatus.NoAccount -> { fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance) } - is TokenStatus.Loaded -> { + is CryptoCurrencyStatus.Loaded -> { fiatBalance = recalculateBalance(status, fiatBalance) } - is TokenStatus.Custom -> { + is CryptoCurrencyStatus.Custom -> { fiatBalance = recalculateBalance(status, fiatBalance) } } @@ -57,7 +57,7 @@ internal class TokenListFiatBalanceOperations( } private fun recalculateBalance( - status: TokenStatus.Loaded, + status: CryptoCurrencyStatus.Loaded, currentBalance: TokenList.FiatBalance, ): TokenList.FiatBalance { return with(currentBalance) { @@ -71,7 +71,7 @@ internal class TokenListFiatBalanceOperations( } private fun recalculateBalance( - status: TokenStatus.Custom, + status: CryptoCurrencyStatus.Custom, currentBalance: TokenList.FiatBalance, ): TokenList.FiatBalance { return with(currentBalance) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 9283eacb78..a39720b230 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -7,9 +7,9 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId @@ -22,7 +22,7 @@ internal class TokenListOperations( private val tokensRepository: TokensRepository, private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, - private val tokens: Set, + private val tokens: Set, private val dispatchers: CoroutineDispatcherProvider, raise: Raise, transform: (Error) -> E, @@ -30,7 +30,7 @@ internal class TokenListOperations( constructor( userWalletId: UserWalletId, - tokens: Set, + tokens: Set, useCase: GetTokenListUseCase, raise: Raise, transform: (Error) -> E, @@ -55,7 +55,7 @@ internal class TokenListOperations( val tokensNes = tokens.toNonEmptySetOrNull() ?: return@withContext TokenList.NotInitialized - val isAnyTokenLoading = tokensNes.any { it.value is TokenStatus.Loading } + val isAnyTokenLoading = tokensNes.any { it.value is CryptoCurrencyStatus.Loading } val fiatBalanceOperations = TokenListFiatBalanceOperations(tokensNes, isAnyTokenLoading, dispatchers) createTokenList( @@ -69,14 +69,14 @@ internal class TokenListOperations( } private suspend fun createTokenList( - tokens: NonEmptySet, + tokens: NonEmptySet, fiatBalance: TokenList.FiatBalance, isAnyTokenLoading: Boolean, isGrouped: Boolean, isSortedByBalance: Boolean, ): TokenList { val sortingOperations = TokenListSortingOperations( - tokens = tokens, + currencies = tokens, isAnyTokenLoading = isAnyTokenLoading, sortByBalance = isSortedByBalance, dispatchers = dispatchers, @@ -90,7 +90,7 @@ internal class TokenListOperations( } private suspend fun createTokenList( - tokens: NonEmptySet, + tokens: NonEmptySet, sortingOperations: TokenListSortingOperations<*>, fiatBalance: TokenList.FiatBalance, isGrouped: Boolean, @@ -108,9 +108,9 @@ internal class TokenListOperations( } } - private suspend fun getNetworks(tokensNes: NonEmptySet): Set { + private suspend fun getNetworks(tokensNes: NonEmptySet): Set { return withContext(dispatchers.io) { - val networksIds = tokensNes.map { it.networkId }.toNonEmptySet() + val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet() catch( block = { networksRepository.getNetworks(networksIds) }, catch = { raise(Error.DataError(it)) }, @@ -124,7 +124,7 @@ internal class TokenListOperations( ): TokenList.Ungrouped = TokenList.Ungrouped( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, - tokens = sortingOperations.getTokens(), + currencies = sortingOperations.getTokens(), ) private suspend fun createGroupedTokenList( @@ -138,13 +138,13 @@ internal class TokenListOperations( ) private fun createUnsortedUngroupedTokenList( - tokens: NonEmptySet, + tokens: NonEmptySet, fiatBalance: TokenList.FiatBalance, ): TokenList.Ungrouped { return TokenList.Ungrouped( sortedBy = TokenList.SortType.NONE, totalFiatBalance = fiatBalance, - tokens = tokens, + currencies = tokens, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index e40520ec97..debb451f54 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -6,16 +6,16 @@ import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal internal class TokenListSortingOperations( - private val tokens: Set, + private val currencies: Set, private val isAnyTokenLoading: Boolean, private val sortByBalance: Boolean, private val dispatchers: CoroutineDispatcherProvider, @@ -31,9 +31,9 @@ internal class TokenListSortingOperations( sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading, ) : this( - tokens = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.tokens }.toSet() - is TokenList.Ungrouped -> tokenList.tokens + currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }.toSet() + is TokenList.Ungrouped -> tokenList.currencies is TokenList.NotInitialized -> emptySet() }, isAnyTokenLoading = isAnyTokenLoading, @@ -45,7 +45,7 @@ internal class TokenListSortingOperations( suspend fun getGroupedTokens(networks: Set): NonEmptySet { return withContext(dispatchers.default) { - ensure(tokens.isNotEmpty()) { Error.EmptyTokens } + ensure(currencies.isNotEmpty()) { Error.EmptyTokens } val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { Error.EmptyNetworks } @@ -58,9 +58,9 @@ internal class TokenListSortingOperations( } } - suspend fun getTokens(): NonEmptySet { + suspend fun getTokens(): NonEmptySet { return withContext(dispatchers.default) { - val tokensNes = ensureNotNull(tokens.toNonEmptySetOrNull()) { + val tokensNes = ensureNotNull(currencies.toNonEmptySetOrNull()) { Error.EmptyTokens } @@ -71,8 +71,8 @@ internal class TokenListSortingOperations( fun getSortType() = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE private fun groupTokens(networks: NonEmptySet): NonEmptySet { - val groupedTokens = tokens - .groupBy { it.networkId } + val groupedTokens = currencies + .groupBy { it.currency.networkId } .map { (networkId, tokens) -> val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { Error.NetworkNotFound(networkId) @@ -80,7 +80,7 @@ internal class TokenListSortingOperations( NetworkGroup( network = network, - tokens = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, + currencies = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, ) } .toNonEmptySetOrNull() @@ -91,9 +91,9 @@ internal class TokenListSortingOperations( private fun groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptySet { val groupsWithSortedTokens = groupTokens(networks) .map { group -> - val tokens = group.tokens as? NonEmptySet + val tokens = group.currencies as? NonEmptySet ?: error("Tokens can not be empty here") - group.copy(tokens = sortTokensByBalance(tokens)) + group.copy(currencies = sortTokensByBalance(tokens)) } .toNonEmptySet() @@ -104,7 +104,7 @@ internal class TokenListSortingOperations( } } - private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { + private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { return if (isAnyTokenLoading) { tokens } else { @@ -117,7 +117,7 @@ internal class TokenListSortingOperations( private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { return groupsWithSortedTokens .sortedByDescending { group -> - group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } + group.currencies.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } } .toNonEmptySetOrNull() ?: error("Tokens can not be empty here") diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 97efc6c34a..36af396bbe 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -15,7 +15,7 @@ interface NetworksRepository { fun getNetworkStatuses( userWalletId: UserWalletId, - networks: Map>, + networks: Map>, refresh: Boolean, ): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index fd3e10aaff..d1aaef5ae1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token import kotlinx.coroutines.flow.Flow /** @@ -9,5 +9,5 @@ import kotlinx.coroutines.flow.Flow * */ interface QuotesRepository { - fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> + fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index c019c0a34d..f999803031 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -11,14 +11,14 @@ interface TokensRepository { suspend fun saveTokens( userWalletId: UserWalletId, - tokens: Set, + currencies: Set, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) - suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token + suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency - fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> + fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> fun isTokensGrouped(userWalletId: UserWalletId): Flow diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index 6a666e453b..ab5b194a5b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -6,7 +6,7 @@ import arrow.core.right import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MockTokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -191,7 +191,7 @@ internal class ApplyTokenListSortingUseCaseTest { private fun getTokensRepository( sortTokensResult: Either = Unit.right(), - tokens: Flow>> = flowOf(MockTokens.tokens.right()), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), ): MockTokensRepository { return MockTokensRepository(sortTokensResult, tokens, emptyFlow(), emptyFlow()) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 8cb8237f21..a1ff4474c9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -9,10 +9,10 @@ import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokenLists import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository import com.tangem.domain.tokens.repository.MockTokensRepository @@ -224,7 +224,7 @@ internal class GetTokenListUseCaseTest { fun `when tokens is empty then not initialized token list should be received`() = runTest { val expectedResult = MockTokenLists.notInitializedTokenList.right() - val useCase = getUseCase(tokens = flowOf(emptySet().right())) + val useCase = getUseCase(tokens = flowOf(emptySet().right())) // When val result = useCase(userWalletId).first() @@ -318,7 +318,7 @@ internal class GetTokenListUseCaseTest { } private fun getUseCase( - tokens: Flow>> = flowOf(MockTokens.tokens.right()), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), quotes: Flow>> = flowOf(MockQuotes.quotes.right()), networks: Either> = MockNetworks.networks.right(), statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index 2e0aab633f..ebda181faf 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -9,22 +9,22 @@ internal object MockNetworksGroups { val networkGroup1 = NetworkGroup( network = MockNetworks.network1, - tokens = MockTokensStates.failedTokenStates - .filter { it.networkId == MockNetworks.network1.id } + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network1.id } .toNonEmptySetOrNull()!!, ) val networkGroup2 = NetworkGroup( network = MockNetworks.network2, - tokens = MockTokensStates.failedTokenStates - .filter { it.networkId == MockNetworks.network2.id } + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network2.id } .toNonEmptySetOrNull()!!, ) val networkGroup3 = NetworkGroup( network = MockNetworks.network3, - tokens = MockTokensStates.failedTokenStates - .filter { it.networkId == MockNetworks.network3.id } + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network3.id } .toNonEmptySetOrNull()!!, ) @@ -32,21 +32,21 @@ internal object MockNetworksGroups { val loadedNetworksGroups = failedNetworksGroups.map { group -> group.copy( - tokens = MockTokensStates.loadedTokensStates - .filter { it.networkId == group.network.id } + currencies = MockTokensStates.loadedTokensStates + .filter { it.currency.networkId == group.network.id } .toNonEmptySetOrNull()!!, ) }.toNonEmptySet() val sortedNetworksGroups = loadedNetworksGroups.map { group -> group.copy( - tokens = group.tokens + currencies = group.currencies .sortedByDescending { it.value.fiatAmount } .toNonEmptySetOrNull()!!, ) } .sortedByDescending { group -> - group.tokens.sumOf { it.value.fiatAmount!! } + group.currencies.sumOf { it.value.fiatAmount!! } } .toNonEmptySetOrNull()!! } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 767391423e..d54da733fd 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -8,61 +8,61 @@ import java.math.BigDecimal internal object MockQuotes { val quote1 = Quote( - tokenId = MockTokens.token1.id, + currencyId = MockTokens.token1.id, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), ) val quote2 = Quote( - tokenId = MockTokens.token2.id, + currencyId = MockTokens.token2.id, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), ) val quote3 = Quote( - tokenId = MockTokens.token3.id, + currencyId = MockTokens.token3.id, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), ) val quote4 = Quote( - tokenId = MockTokens.token4.id, + currencyId = MockTokens.token4.id, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), ) val quote5 = Quote( - tokenId = MockTokens.token5.id, + currencyId = MockTokens.token5.id, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), ) val quote6 = Quote( - tokenId = MockTokens.token6.id, + currencyId = MockTokens.token6.id, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), ) val quote7 = Quote( - tokenId = MockTokens.token7.id, + currencyId = MockTokens.token7.id, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), ) val quote8 = Quote( - tokenId = MockTokens.token8.id, + currencyId = MockTokens.token8.id, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), ) val quote9 = Quote( - tokenId = MockTokens.token9.id, + currencyId = MockTokens.token9.id, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), ) val quote10 = Quote( - tokenId = MockTokens.token10.id, + currencyId = MockTokens.token10.id, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 604a2cbffc..403dcb5046 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -5,8 +5,8 @@ import arrow.core.toNonEmptySetOrNull import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -24,7 +24,7 @@ internal object MockTokenLists { ) val emptyUngroupedTokenList = TokenList.Ungrouped( - tokens = emptySet(), + currencies = emptySet(), totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) @@ -36,14 +36,14 @@ internal object MockTokenLists { ) val failedUngroupedTokenList = TokenList.Ungrouped( - tokens = MockTokensStates.failedTokenStates, + currencies = MockTokensStates.failedTokenStates, totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toNonEmptySetOrNull()!!, + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptySetOrNull()!!, totalFiatBalance = TokenList.FiatBalance.Loading, ) } @@ -53,8 +53,8 @@ internal object MockTokenLists { totalFiatBalance = TokenList.FiatBalance.Loading, groups = groups.map { group -> group.copy( - tokens = group.tokens - .map { it.copy(value = TokenStatus.Loading) } + currencies = group.currencies + .map { it.copy(value = CryptoCurrencyStatus.Loading) } .toNonEmptySetOrNull()!!, ) }.toNonEmptySetOrNull()!!, @@ -66,7 +66,7 @@ internal object MockTokenLists { val tokens = MockTokensStates.loadedTokensStates return failedUngroupedTokenList.copy( - tokens = tokens, + currencies = tokens, sortedBy = TokenList.SortType.NONE, totalFiatBalance = TokenList.FiatBalance.Loaded( amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, @@ -84,7 +84,7 @@ internal object MockTokenLists { sortedBy = TokenList.SortType.NONE, totalFiatBalance = TokenList.FiatBalance.Loaded( amount = groups - .flatMap { it.tokens as NonEmptySet } + .flatMap { it.currencies as NonEmptySet } .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, ), @@ -98,7 +98,7 @@ internal object MockTokenLists { .toNonEmptySetOrNull()!! return unsortedUngroupedTokenList.copy( - tokens = tokens, + currencies = tokens, sortedBy = TokenList.SortType.BALANCE, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index 53703985ec..b0af037c5e 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -1,119 +1,123 @@ package com.tangem.domain.tokens.mock -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency internal object MockTokens { - val token1 = Token( - id = Token.ID("token1"), - networkId = MockNetworks.network1.id, - name = "Token 1", - symbol = "T1", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token2 = Token( - id = Token.ID("token2"), - networkId = MockNetworks.network1.id, - name = "Token 2", - symbol = "T2", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token3 = Token( - id = Token.ID("token3"), - networkId = MockNetworks.network1.id, - name = "Token 3", - symbol = "T3", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token4 = Token( - id = Token.ID("token4"), - networkId = MockNetworks.network2.id, - name = "Token 4", - symbol = "T4", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token5 = Token( - id = Token.ID("token5"), - networkId = MockNetworks.network2.id, - name = "Token 5", - symbol = "T5", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token6 = Token( - id = Token.ID("token6"), - networkId = MockNetworks.network2.id, - name = "Token 6", - symbol = "T6", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token7 = Token( - id = Token.ID("token7"), - networkId = MockNetworks.network3.id, - name = "Token 7", - symbol = "T7", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = null, - derivationPath = null, - ) - val token8 = Token( - id = Token.ID("token8"), - networkId = MockNetworks.network3.id, - name = "Token 8", - symbol = "T8", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token9 = Token( - id = Token.ID("token9"), - networkId = MockNetworks.network3.id, - name = "Token 9", - symbol = "T9", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token10 = Token( - id = Token.ID("token10"), - networkId = MockNetworks.network3.id, - name = "Token 10", - symbol = "T10", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) + val token1 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token2 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token3 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token4 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token5 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token6 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token7 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token8 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token9 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token10 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) val tokens = setOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 6652b1bd69..4df0a5d525 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,120 +1,60 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.TokenStatus @Suppress("MemberVisibilityCanBePrivate") internal object MockTokensStates { - val tokenState1 = TokenStatus( - id = MockTokens.token1.id, - networkId = MockTokens.token1.networkId, - name = MockTokens.token1.name, - symbol = MockTokens.token1.symbol, - decimals = MockTokens.token1.decimals, - iconUrl = MockTokens.token1.iconUrl, - isCoin = MockTokens.token1.contractAddress == null, - value = TokenStatus.Unreachable, + val tokenState1 = CryptoCurrencyStatus( + currency = MockTokens.token1, + value = CryptoCurrencyStatus.Unreachable, ) - val tokenState2 = TokenStatus( - id = MockTokens.token2.id, - networkId = MockTokens.token2.networkId, - name = MockTokens.token2.name, - symbol = MockTokens.token2.symbol, - decimals = MockTokens.token2.decimals, - iconUrl = MockTokens.token2.iconUrl, - isCoin = MockTokens.token2.contractAddress == null, - value = TokenStatus.Unreachable, + val tokenState2 = CryptoCurrencyStatus( + currency = MockTokens.token2, + value = CryptoCurrencyStatus.Unreachable, ) - val tokenState3 = TokenStatus( - id = MockTokens.token3.id, - networkId = MockTokens.token3.networkId, - name = MockTokens.token3.name, - symbol = MockTokens.token3.symbol, - decimals = MockTokens.token3.decimals, - iconUrl = MockTokens.token3.iconUrl, - isCoin = MockTokens.token3.contractAddress == null, - value = TokenStatus.Unreachable, + val tokenState3 = CryptoCurrencyStatus( + currency = MockTokens.token3, + value = CryptoCurrencyStatus.Unreachable, ) - val tokenState4 = TokenStatus( - id = MockTokens.token4.id, - networkId = MockTokens.token4.networkId, - name = MockTokens.token4.name, - symbol = MockTokens.token4.symbol, - decimals = MockTokens.token4.decimals, - iconUrl = MockTokens.token4.iconUrl, - isCoin = MockTokens.token4.contractAddress == null, - value = TokenStatus.MissedDerivation, + val tokenState4 = CryptoCurrencyStatus( + currency = MockTokens.token4, + value = CryptoCurrencyStatus.MissedDerivation, ) - val tokenState5 = TokenStatus( - id = MockTokens.token5.id, - networkId = MockTokens.token5.networkId, - name = MockTokens.token5.name, - symbol = MockTokens.token5.symbol, - decimals = MockTokens.token5.decimals, - iconUrl = MockTokens.token5.iconUrl, - isCoin = MockTokens.token5.contractAddress == null, - value = TokenStatus.MissedDerivation, + val tokenState5 = CryptoCurrencyStatus( + currency = MockTokens.token5, + value = CryptoCurrencyStatus.MissedDerivation, ) - val tokenState6 = TokenStatus( - id = MockTokens.token6.id, - networkId = MockTokens.token6.networkId, - name = MockTokens.token6.name, - symbol = MockTokens.token6.symbol, - decimals = MockTokens.token6.decimals, - iconUrl = MockTokens.token6.iconUrl, - isCoin = MockTokens.token6.contractAddress == null, - value = TokenStatus.MissedDerivation, + val tokenState6 = CryptoCurrencyStatus( + currency = MockTokens.token6, + value = CryptoCurrencyStatus.MissedDerivation, ) - val tokenState7 = TokenStatus( - id = MockTokens.token7.id, - networkId = MockTokens.token7.networkId, - name = MockTokens.token7.name, - symbol = MockTokens.token7.symbol, - decimals = MockTokens.token7.decimals, - iconUrl = MockTokens.token7.iconUrl, - isCoin = MockTokens.token7.contractAddress == null, - value = TokenStatus.NoAccount, + val tokenState7 = CryptoCurrencyStatus( + currency = MockTokens.token7, + value = CryptoCurrencyStatus.NoAccount, ) - val tokenState8 = TokenStatus( - id = MockTokens.token8.id, - networkId = MockTokens.token8.networkId, - name = MockTokens.token8.name, - symbol = MockTokens.token8.symbol, - decimals = MockTokens.token8.decimals, - iconUrl = MockTokens.token8.iconUrl, - isCoin = MockTokens.token8.contractAddress == null, - value = TokenStatus.NoAccount, + val tokenState8 = CryptoCurrencyStatus( + currency = MockTokens.token8, + value = CryptoCurrencyStatus.NoAccount, ) - val tokenState9 = TokenStatus( - id = MockTokens.token9.id, - networkId = MockTokens.token9.networkId, - name = MockTokens.token9.name, - symbol = MockTokens.token9.symbol, - decimals = MockTokens.token9.decimals, - iconUrl = MockTokens.token9.iconUrl, - isCoin = MockTokens.token9.contractAddress == null, - value = TokenStatus.NoAccount, + val tokenState9 = CryptoCurrencyStatus( + currency = MockTokens.token9, + value = CryptoCurrencyStatus.NoAccount, ) - val tokenState10 = TokenStatus( - id = MockTokens.token10.id, - networkId = MockTokens.token10.networkId, - name = MockTokens.token10.name, - symbol = MockTokens.token10.symbol, - decimals = MockTokens.token10.decimals, - iconUrl = MockTokens.token10.iconUrl, - isCoin = MockTokens.token10.contractAddress == null, - value = TokenStatus.NoAccount, + val tokenState10 = CryptoCurrencyStatus( + currency = MockTokens.token10, + value = CryptoCurrencyStatus.NoAccount, ) val failedTokenStates = nonEmptySetOf( @@ -130,14 +70,15 @@ internal object MockTokensStates { tokenState10, ) - val loadedTokensStates = failedTokenStates.map { state -> - val networkStatus = MockNetworks.verifiedNetworksStatuses.first { it.networkId == state.networkId } - val amount = (networkStatus.value as NetworkStatus.Verified).amounts[state.id]!! - val quote = MockQuotes.quotes.first { it.tokenId == state.id } + val loadedTokensStates = failedTokenStates.map { status -> + val networkStatus = MockNetworks.verifiedNetworksStatuses + .first { it.networkId == status.currency.networkId } + val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! + val quote = MockQuotes.quotes.first { it.currencyId == status.currency.id } val fiatAmount = amount * quote.fiatRate - state.copy( - value = TokenStatus.Loaded( + status.copy( + value = CryptoCurrencyStatus.Loaded( amount = amount, fiatAmount = fiatAmount, fiatRate = quote.fiatRate, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 98c7002327..649cd7808c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -3,9 +3,9 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -21,7 +21,7 @@ internal class MockNetworksRepository( override fun getNetworkStatuses( userWalletId: UserWalletId, - networks: Map>, + networks: Map>, refresh: Boolean, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index fcf52e44e4..9ec660b113 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -3,8 +3,8 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,7 +12,7 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index bffa7f0ac5..8e66695d8f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -4,19 +4,19 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map internal class MockTokensRepository( private val sortTokensResult: Either, - private val tokens: Flow>>, + private val tokens: Flow>>, private val isGrouped: Flow>, private val isSortedByBalance: Flow>, ) : TokensRepository { - var tokensIdsAfterSortingApply: Set? = null + var tokensIdsAfterSortingApply: Set? = null private set var isTokensGroupedAfterSortingApply: Boolean? = null @@ -27,22 +27,25 @@ internal class MockTokensRepository( override suspend fun saveTokens( userWalletId: UserWalletId, - tokens: Set, + currencies: Set, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) { sortTokensResult.onLeft { throw it } - tokensIdsAfterSortingApply = tokens + tokensIdsAfterSortingApply = currencies isTokensGroupedAfterSortingApply = isGroupedByNetwork isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } - override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { + override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return MockTokens.token1 } - override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + override fun getMultiCurrencyWalletCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { return tokens.map { it.getOrElse { e -> throw e } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt similarity index 63% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index b654b47713..75084dad74 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -3,49 +3,50 @@ package com.tangem.feature.wallet.presentation.wallet.utils import androidx.annotation.DrawableRes import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.utils.converter.Converter import java.math.BigDecimal -internal class TokenStatusToTokenItemConverter( +internal class CryptoCurrencyStatusToTokenItemConverter( private val isWalletContentHidden: Boolean, private val fiatCurrencyCode: String, private val fiatCurrencySymbol: String, -) : Converter { +) : Converter { - private val TokenStatus.networkIconResId: Int? + private val CryptoCurrencyStatus.networkIconResId: Int? @DrawableRes get() { // TODO: [REDACTED_JIRA] - return if (isCoin) null else R.drawable.img_eth_22 + return if (currency is CryptoCurrency.Token) null else R.drawable.img_eth_22 } - private val TokenStatus.tokenIconResId: Int + private val CryptoCurrencyStatus.tokenIconResId: Int @DrawableRes get() { // TODO: [REDACTED_JIRA] return R.drawable.img_eth_22 } - override fun convert(value: TokenStatus): TokenItemState { + override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { - is TokenStatus.Loading -> TokenItemState.Loading - is TokenStatus.Loaded, - is TokenStatus.Custom, + is CryptoCurrencyStatus.Loading -> TokenItemState.Loading + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, -> value.mapToTokenItemState() // TODO: Add other token item states, currently not designed - is TokenStatus.MissedDerivation, - is TokenStatus.NoAccount, - is TokenStatus.Unreachable, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Unreachable, -> value.mapToUnreachableTokenItemState() } } - private fun TokenStatus.mapToTokenItemState(): TokenItemState.Content { + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { return TokenItemState.Content( - id = this.id.value, - name = this.name, - tokenIconUrl = this.iconUrl, + id = currency.id.value, + name = currency.name, + tokenIconUrl = currency.iconUrl, tokenIconResId = this.tokenIconResId, networkIconResId = this.networkIconResId, amount = getFormattedAmount(), @@ -61,27 +62,27 @@ internal class TokenStatusToTokenItemConverter( ) } - private fun TokenStatus.getFormattedAmount(): String { + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN - return BigDecimalFormatter.formatCryptoAmount(amount, symbol, decimals) + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } - private fun TokenStatus.getFormattedFiatAmount(): String { + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol) } - private fun TokenStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( - id = this.id.value, - name = this.name, - tokenIconUrl = this.iconUrl, + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = currency.id.value, + name = currency.name, + tokenIconUrl = currency.iconUrl, tokenIconResId = this.tokenIconResId, networkIconResId = this.networkIconResId, ) - private fun TokenStatus.getPriceChangeConfig(): PriceChangeConfig { + private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { val priceChange = value.priceChange ?: return PriceChangeConfig(UNKNOWN_AMOUNT_SIGN, PriceChangeConfig.Type.DOWN) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index f4f1d3803f..574cb11482 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.utils +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState.TokensListItemState import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens @@ -19,7 +19,7 @@ internal class TokenListToContentItemsConverter( private val clickCallbacks: WalletClickCallbacks, ) : Converter { - private val tokenStatusConverter = TokenStatusToTokenItemConverter( + private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( isWalletContentHidden = isWalletContentHidden, fiatCurrencyCode = fiatCurrencyCode, fiatCurrencySymbol = fiatCurrencySymbol, @@ -47,7 +47,7 @@ internal class TokenListToContentItemsConverter( } private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList { - return tokens.fold(initial = persistentListOf()) { acc, token -> + return currencies.fold(initial = persistentListOf()) { acc, token -> acc.mutate { it.addToken(token) } } } @@ -55,14 +55,14 @@ internal class TokenListToContentItemsConverter( private fun MutableList.addGroup(group: NetworkGroup): List { this.add(TokensListItemState.NetworkGroupTitle(group.network.name)) - group.tokens.forEach { token -> + group.currencies.forEach { token -> this.addToken(token) } return this } - private fun MutableList.addToken(token: TokenStatus): List { + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { val tokenItemState = tokenStatusConverter.convert(token) this.add(TokensListItemState.Token(tokenItemState)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index a5bbac817e..1bdd9dd004 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import com.tangem.common.Provider import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TokenStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder @@ -123,12 +123,12 @@ internal class WalletNotificationsListFactory( private fun TokenList.hasMissedDerivations(): Boolean { val statuses = when (this) { - is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::tokens).map(TokenStatus::value) - is TokenList.Ungrouped -> tokens.map(TokenStatus::value) + is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies).map(CryptoCurrencyStatus::value) + is TokenList.Ungrouped -> currencies.map(CryptoCurrencyStatus::value) TokenList.NotInitialized -> emptyList() } - return statuses.any { it is TokenStatus.MissedDerivation } + return statuses.any { it is CryptoCurrencyStatus.MissedDerivation } } private companion object { From 23aac9041cf96a3ce517bc55b3ac8ce8173f0081 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Aug 2023 14:31:41 +0300 Subject: [PATCH 086/100] Updated on 2026-08-14 --- .../tangem/data/tokens/mock/MockNetworks.kt | 2 + .../mapper/GetWalletTokenErrorMappers.kt | 1 + .../error/mapper/TokenListErrorMappers.kt | 1 + .../domain/tokens/model/CryptoCurrency.kt | 26 ++++++++++ .../tokens/model/CryptoCurrencyStatus.kt | 47 +++++++++++++++++++ .../com/tangem/domain/tokens/model/Network.kt | 13 +++++ .../domain/tokens/model/NetworkGroup.kt | 8 ++++ .../domain/tokens/model/NetworkStatus.kt | 39 ++++++++++++--- .../com/tangem/domain/tokens/model/Quote.kt | 7 +++ .../tangem/domain/tokens/model/TokenList.kt | 42 +++++++++++++++++ .../CurrenciesStatusesOperations.kt | 4 ++ .../operations/CurrencyStatusOperations.kt | 41 ++++++++-------- .../tokens/repository/NetworksRepository.kt | 14 ++++++ .../tokens/repository/QuotesRepository.kt | 7 +++ .../tokens/repository/TokensRepository.kt | 34 ++++++++++++++ .../tangem/domain/tokens/mock/MockNetworks.kt | 3 ++ 16 files changed, 263 insertions(+), 26 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt index af85a6322b..12256c2120 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt @@ -32,6 +32,7 @@ internal object MockNetworks { MockTokens.token2.id to BigDecimal("42.2"), MockTokens.token3.id to BigDecimal("1000000000.5"), ), + hasTransactionsInProgress = false, ), ) @@ -49,6 +50,7 @@ internal object MockNetworks { MockTokens.token9.id to BigDecimal.TEN, MockTokens.token10.id to BigDecimal.TEN, ), + hasTransactionsInProgress = false, ), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt index 1252251389..e129b9fc6f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -9,6 +9,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenError(): TokenError { is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, -> TokenError.UnableToCreateToken } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 9b24b82737..4796f50063 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -10,6 +10,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt index 891d75986e..504e7ef421 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -1,5 +1,17 @@ package com.tangem.domain.tokens.model +/** + * Represents a generic cryptocurrency. + * + * @property id Unique identifier for the cryptocurrency. + * @property networkId Identifier for the network to which the cryptocurrency belongs. + * @property name Human-readable name of the cryptocurrency. + * @property symbol Symbol of the cryptocurrency. + * @property decimals Number of decimal places used by the cryptocurrency. + * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. + * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the + * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. + */ sealed class CryptoCurrency { abstract val id: ID @@ -10,6 +22,9 @@ sealed class CryptoCurrency { abstract val iconUrl: String? abstract val derivationPath: String? + /** + * Represents a native coin in the blockchain network. + */ data class Coin( override val id: ID, override val networkId: Network.ID, @@ -25,6 +40,12 @@ sealed class CryptoCurrency { } } + /** + * Represents a token in the blockchain network, typically a non-native asset. + * + * @property contractAddress Address of the contract managing the token. + * @property isCustom Indicates whether the token is a custom user-added token or not. + */ data class Token( override val id: ID, override val networkId: Network.ID, @@ -43,6 +64,11 @@ sealed class CryptoCurrency { } } + /** + * Value class for uniquely identifying a cryptocurrency. + * + * @property value The unique identifier value. + */ @JvmInline value class ID(val value: String) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index f67fc23e5a..a735f3ed06 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -2,27 +2,64 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal +/** + * Represents the status of a cryptocurrency asset within a network. + * + * This class encapsulates the details of a specific cryptocurrency, either a coin or token, + * along with its current status within the blockchain network. The status can include various states + * like Loading, Unreachable, Loaded, etc. + * + * @property currency The details of the cryptocurrency asset, including its type, name, symbol, and other properties. + * @property value The current status of the cryptocurrency, reflecting its state within the network. + */ data class CryptoCurrencyStatus( val currency: CryptoCurrency, val value: Status, ) { + /** + * Represents the various states a token can have, encapsulating different information based on the state. + */ sealed class Status { + + /** The amount of the token. */ open val amount: BigDecimal? = null + + /** The fiat equivalent of the token's amount. */ open val fiatAmount: BigDecimal? = null + + /** The exchange rate used for converting the token amount to fiat. */ open val fiatRate: BigDecimal? = null + + /** The change in price of the token. */ open val priceChange: BigDecimal? = null + + /** Indicates if there are any transactions in progress related to the token. */ open val hasTransactionsInProgress: Boolean = false } + /** Represents the Loading state of a token, typically while fetching its details. */ object Loading : Status() + /** Represents a state where the token is not reachable. */ object Unreachable : Status() + /** Represents a state where the token's derivation is missed. */ object MissedDerivation : Status() + /** Represents a state where there is no account associated with the token. */ object NoAccount : Status() + /** + * Represents a Loaded state of a token with complete information. + * + * @property amount The amount of the token. + * @property fiatAmount The fiat equivalent of the token's amount. + * @property fiatRate The exchange rate used for converting the token amount to fiat. + * @property priceChange The change in price of the token. + * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token + * network. + */ data class Loaded( override val amount: BigDecimal, override val fiatAmount: BigDecimal, @@ -31,6 +68,16 @@ data class CryptoCurrencyStatus( override val hasTransactionsInProgress: Boolean, ) : Status() + /** + * Represents a Custom state of a token, typically used for user-defined tokens. + * + * @property amount The amount of the token. + * @property fiatAmount The fiat equivalent of the token's amount (optional). + * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). + * @property priceChange The change in price of the token (optional). + * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token + * network. + */ data class Custom( override val amount: BigDecimal, override val fiatAmount: BigDecimal?, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt index f7bd4500c2..402ae364eb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt @@ -1,5 +1,13 @@ package com.tangem.domain.tokens.model +/** + * Represents a blockchain network, identified by a unique ID and a human-readable name. + * + * @property id The unique identifier of the network, encapsulated as an inline value class. + * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". + * + * @throws IllegalArgumentException If the name or ID is blank. + */ data class Network( val id: ID, val name: String, @@ -9,6 +17,11 @@ data class Network( require(name.isNotBlank()) { "Network name must not be blank" } } + /** + * Represents a unique identifier for a network. + * + * @property value The string value of the network ID. + */ @JvmInline value class ID(val value: String) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index 51d03a899e..b244d7252f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -1,5 +1,13 @@ package com.tangem.domain.tokens.model +/** + * Represents a group of cryptocurrencies associated with a specific network. + * + * This class encapsulates a collection of cryptocurrency statuses, all of which are part of the same blockchain network. + * + * @property network The blockchain network associated with the group. + * @property currencies A set of cryptocurrency statuses that belong to the network. + */ data class NetworkGroup( val network: Network, val currencies: Set, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index f76108f535..454480611c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -2,22 +2,49 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal +/** + * Represents the status of a specific blockchain network. + * + * @property networkId The unique identifier of the network for which the status is provided. + * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. + */ data class NetworkStatus( val networkId: Network.ID, val value: Status, ) { - sealed class Status { - open val amounts: Map? = null - } + /** + * Represents the various possible statuses of a network. + * + * This sealed class includes different states like unreachable, missed derivation, verified, and no account. + */ + sealed class Status + /** + * Represents the state where the network is unreachable. + */ object Unreachable : Status() + /** + * Represents the state where a derivation has been missed. + */ object MissedDerivation : Status() - data class TransactionInProgress(override val amounts: Map) : Status() - - data class Verified(override val amounts: Map) : Status() + /** + * Represents the verified state of the network, including the amounts associated with different cryptocurrencies and whether there are transactions in progress. + * + * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. + * @property hasTransactionsInProgress A boolean indicating whether there are transactions in progress within the network. + */ + data class Verified( + val amounts: Map, + val hasTransactionsInProgress: Boolean, + ) : Status() + /** + * Represents the state where there is no account, and an amount is required to create one. + * + * @property amountToCreateAccount The amount required to create an account within the network. + */ data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt index 192c196537..7247a75ed2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt @@ -2,6 +2,13 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal +/** + * Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change. + * + * @property currencyId The unique identifier of the cryptocurrency for which the quote is provided. + * @property fiatRate The current fiat exchange rate for the cryptocurrency. + * @property priceChange The price change for the cryptocurrency. + */ data class Quote( val currencyId: CryptoCurrency.ID, val fiatRate: BigDecimal, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index f19f35bef2..55327b82da 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -2,33 +2,75 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal +/** + * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. + * + * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. + * Additional details like the total fiat balance and the sorting type can be associated with the list. + * + * @property totalFiatBalance The total fiat balance across all tokens, which could be in a loading state, failed, or loaded with a specific amount. + * @property sortedBy The criteria used for sorting the tokens. + */ sealed class TokenList { open val totalFiatBalance: FiatBalance = FiatBalance.Loading open val sortedBy: SortType = SortType.NONE + /** + * Represents tokens that are grouped by their network. + * + * @property groups A set of network groups containing tokens. + * @property totalFiatBalance The total fiat balance across all groups. + * @property sortedBy The criteria used for sorting the tokens within the groups. + */ data class GroupedByNetwork( val groups: Set, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() + /** + * Represents tokens that are not grouped by any specific criteria. + * + * @property currencies A set of cryptocurrency statuses. + * @property totalFiatBalance The total fiat balance across all currencies. + * @property sortedBy The criteria used for sorting the currencies. + */ data class Ungrouped( val currencies: Set, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() + /** Represents a state where the token list is not initialized. */ object NotInitialized : TokenList() + /** Defines the possible sorting criteria for the tokens. */ enum class SortType { NONE, BALANCE, } + /** + * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount. + */ sealed class FiatBalance { + /** + * Represents the loading state of the fiat balance. + * This state indicates that the fiat balance is currently being retrieved or calculated. + */ object Loading : FiatBalance() + /** + * Represents the failure state of the fiat balance. + * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. + */ object Failed : FiatBalance() + /** + * Represents the successfully loaded state of the fiat balance. + * + * @property amount The loaded fiat balance amount. + * @property isAllAmountsSummarized Indicates whether the amount includes a summary of all underlying amounts. + */ data class Loaded( val amount: BigDecimal, val isAllAmountsSummarized: Boolean, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index e64237f1a3..41b8fbae2e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -101,6 +101,8 @@ internal class CurrenciesStatusesOperations( quote = quote, networkStatus = networkStatus, dispatchers = dispatchers, + raise = this, + transformError = { Error.UnableToCreateCurrencyStatus }, ) return currencyStatusOperations.createTokenStatus() @@ -161,6 +163,8 @@ internal class CurrenciesStatusesOperations( object EmptyNetworksStatuses : Error() + object UnableToCreateCurrencyStatus : Error() + data class DataError(val cause: Throwable) : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index a48964936b..50f348f3d3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -1,5 +1,8 @@ package com.tangem.domain.tokens.operations +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus @@ -8,18 +11,17 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal -internal class CurrencyStatusOperations( +internal class CurrencyStatusOperations( private val currency: CryptoCurrency, private val quote: Quote?, private val networkStatus: NetworkStatus?, private val dispatchers: CoroutineDispatcherProvider, -) { + raise: Raise, + transformError: (Error) -> OtherError, +) : DelegatedRaise(raise, transformError) { suspend fun createTokenStatus(): CryptoCurrencyStatus = withContext(dispatchers.default) { - CryptoCurrencyStatus( - currency = currency, - value = createStatus(), - ) + CryptoCurrencyStatus(currency, createStatus()) } private fun createStatus(): CryptoCurrencyStatus.Status { @@ -28,23 +30,22 @@ internal class CurrencyStatusOperations( is NetworkStatus.MissedDerivation -> CryptoCurrencyStatus.MissedDerivation is NetworkStatus.Unreachable -> CryptoCurrencyStatus.Unreachable is NetworkStatus.NoAccount -> CryptoCurrencyStatus.NoAccount - is NetworkStatus.TransactionInProgress, - is NetworkStatus.Verified, - -> createStatus( - amount = getTokenAmount(), - hasTransactionsInProgress = status is NetworkStatus.TransactionInProgress, - ) + is NetworkStatus.Verified -> createStatus(status) } } - private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): CryptoCurrencyStatus.Status { + private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { + val amount = ensureNotNull(status.amounts[currency.id]) { + Error.UnableToFindAmount(currency.id) + } + return when { currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, - hasTransactionsInProgress = hasTransactionsInProgress, + hasTransactionsInProgress = status.hasTransactionsInProgress, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -52,16 +53,11 @@ internal class CurrencyStatusOperations( fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, priceChange = quote.priceChange, - hasTransactionsInProgress = hasTransactionsInProgress, + hasTransactionsInProgress = status.hasTransactionsInProgress, ) } } - private fun getTokenAmount(): BigDecimal { - val amount = networkStatus?.value?.amounts?.get(currency.id) - return amount ?: error("Incorrect network status: $networkStatus") - } - private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? { if (fiatRate == null) return null @@ -71,4 +67,9 @@ internal class CurrencyStatusOperations( private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { return amount * fiatRate } + + sealed class Error { + + data class UnableToFindAmount(val currencyId: CryptoCurrency.ID) : Error() + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 36af396bbe..dfb3a25ef1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -11,8 +11,22 @@ import kotlinx.coroutines.flow.Flow * */ interface NetworksRepository { + /** + * Retrieves the details of the specified blockchain networks, identified by their unique IDs. + * + * @param networksIds The unique identifiers of the networks to be retrieved. + * @return A set of [Network] objects corresponding to the specified network IDs. + */ fun getNetworks(networksIds: Set): Set + /** + * Retrieves the statuses of specified blockchain networks for a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A map of network IDs to sets of cryptocurrency IDs, representing the networks for which statuses are to be retrieved. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. + */ fun getNetworkStatuses( userWalletId: UserWalletId, networks: Map>, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index d1aaef5ae1..1c783ce342 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -9,5 +9,12 @@ import kotlinx.coroutines.flow.Flow * */ interface QuotesRepository { + /** + * Retrieves the quotes for a set of specified cryptocurrencies, identified by their unique IDs. + * + * @param tokensIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. + */ fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index f999803031..865c5fc4da 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -9,6 +9,15 @@ import kotlinx.coroutines.flow.Flow * */ interface TokensRepository { + /** + * Saves the given set of cryptocurrencies, along with the preferences for grouping and sorting, for a specific + * multi-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The set of cryptocurrencies to be saved. + * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. + * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. + */ suspend fun saveTokens( userWalletId: UserWalletId, currencies: Set, @@ -16,11 +25,36 @@ interface TokensRepository { isSortedByBalance: Boolean, ) + /** + * Retrieves the primary cryptocurrency for a specific single-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @return The primary cryptocurrency associated with the user wallet. + */ suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency + /** + * Retrieves the set of cryptocurrencies within a multi-currency wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. + */ fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> + /** + * Determines whether the tokens within a specific multi-currency user wallet are grouped. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. + */ fun isTokensGrouped(userWalletId: UserWalletId): Flow + /** + * Determines whether the tokens within a specific multi-currency user wallet are sorted by balance. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. + */ fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 03ad43ba70..a2194499b2 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -55,6 +55,7 @@ internal object MockNetworks { MockTokens.token2.id to BigDecimal.TEN, MockTokens.token3.id to BigDecimal.TEN, ), + hasTransactionsInProgress = false, ), ) @@ -66,6 +67,7 @@ internal object MockNetworks { MockTokens.token5.id to BigDecimal.TEN, MockTokens.token6.id to BigDecimal.TEN, ), + hasTransactionsInProgress = false, ), ) @@ -78,6 +80,7 @@ internal object MockNetworks { MockTokens.token9.id to BigDecimal.TEN, MockTokens.token10.id to BigDecimal.TEN, ), + hasTransactionsInProgress = false, ), ) From 49904463e0aed2959116f3753def189f976a36f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Aug 2023 21:00:21 +0800 Subject: [PATCH 087/100] Updated on 2026-08-14 --- .../domain/WalletAdditionalInfoFactory.kt | 43 +++++++++++++++++++ .../WalletLoadedTokensListConverter.kt | 13 ++++-- .../factory/WalletSkeletonStateConverter.kt | 29 +++++++------ .../state/factory/WalletStateFactory.kt | 16 ++++--- .../utils/FiatBalanceToWalletCardConverter.kt | 10 ++++- .../utils/TokenListToContentItemsConverter.kt | 6 +-- .../utils/TokenListToWalletStateConverter.kt | 10 +++-- ...lickCallbacks.kt => WalletClickIntents.kt} | 2 +- .../WalletNotificationsListFactory.kt | 14 +++--- .../wallet/viewmodels/WalletViewModel.kt | 6 +-- 10 files changed, 108 insertions(+), 41 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/{WalletClickCallbacks.kt => WalletClickIntents.kt} (91%) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt new file mode 100644 index 0000000000..affbdde057 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.utils.toFormattedCurrencyString +import java.math.BigDecimal + +/** + * Wallet additional info factory + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize strings [REDACTED_JIRA] +internal object WalletAdditionalInfoFactory { + + /** + * Get additional info + * + * @param cardTypesResolver card types resolver + * @param isLocked check if wallet is locked + * @param currencyAmount amount of currency + */ + fun resolve(cardTypesResolver: CardTypesResolver, isLocked: Boolean, currencyAmount: BigDecimal? = null): String { + return if (cardTypesResolver.isMultiwalletAllowed()) { + val backupInfo = "${cardTypesResolver.getBackupCardsCount()} cards" + when { + cardTypesResolver.isWallet2() && !isLocked -> "$backupInfo • Seed phrase" + cardTypesResolver.isTangemWallet() && !isLocked -> backupInfo + isLocked -> "$backupInfo • Locked" + else -> "" + } + } else { + if (isLocked) { + "Locked" + } else { + val blockchain = cardTypesResolver.getBlockchain() + currencyAmount?.toFormattedCurrencyString( + decimals = blockchain.decimals(), + currency = blockchain.currency, + ).orEmpty() + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index edcbbdd1e5..293d66e1bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -2,33 +2,38 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import arrow.core.Either import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter /** * Converter from loaded [TokenListError] or [TokenList] to [WalletStateHolder] * - * @property currentStateProvider current ui state provider + * @property currentStateProvider current ui state provider + * @param cardTypeResolverProvider card type resolver + * @param clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, - clickCallbacks: WalletClickCallbacks, + cardTypeResolverProvider: Provider, + clickIntents: WalletClickIntents, ) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, + cardTypeResolverProvider = cardTypeResolverProvider, isWalletContentHidden = false, // TODO: [REDACTED_JIRA] fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] - clickCallbacks = clickCallbacks, + clickIntents = clickIntents, ) private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index dfdf7abdb3..72419fce8b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -6,11 +6,12 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -19,12 +20,12 @@ import kotlinx.coroutines.flow.flow /** * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder] * - * @property clickCallbacks screen click callbacks + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletSkeletonStateConverter( - private val clickCallbacks: WalletClickCallbacks, + private val clickIntents: WalletClickIntents, ) : Converter, WalletStateHolder> { override fun convert(value: List): WalletStateHolder { @@ -39,13 +40,13 @@ internal class WalletSkeletonStateConverter( private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { return WalletStateHolder.MultiCurrencyContent( - onBackClick = clickCallbacks::onBackClick, + onBackClick = clickIntents::onBackClick, topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), tokensListState = WalletTokensListState.Content( items = persistentListOf(), - onOrganizeTokensClick = clickCallbacks::onOrganizeTokensClick, + onOrganizeTokensClick = clickIntents::onOrganizeTokensClick, ), notifications = persistentListOf(), bottomSheet = null, @@ -57,7 +58,7 @@ internal class WalletSkeletonStateConverter( cardTypeResolver: CardTypesResolver, ): WalletStateHolder.SingleCurrencyContent { return WalletStateHolder.SingleCurrencyContent( - onBackClick = clickCallbacks::onBackClick, + onBackClick = clickIntents::onBackClick, topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(wallets), pullToRefreshConfig = createPullToRefreshConfig(), @@ -75,8 +76,8 @@ internal class WalletSkeletonStateConverter( private fun createTopBarConfig(): WalletTopBarConfig { return WalletTopBarConfig( - onScanCardClick = clickCallbacks::onScanCardClick, - onMoreClick = clickCallbacks::onDetailsClick, + onScanCardClick = clickIntents::onScanCardClick, + onMoreClick = clickIntents::onDetailsClick, ) } @@ -84,18 +85,22 @@ internal class WalletSkeletonStateConverter( return WalletsListConfig( selectedWalletIndex = 0, wallets = wallets.map { wallet -> + val cardTypeResolver = wallet.scanResponse.cardTypesResolver WalletCardState.Loading( id = wallet.walletId, title = wallet.name, - additionalInfo = "", // TODO add additional info resolver - imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver), + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + isLocked = wallet.isLocked, + ), + imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), ) }.toImmutableList(), - onWalletChange = clickCallbacks::onWalletChange, + onWalletChange = clickIntents::onWalletChange, ) } private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickCallbacks::onRefreshSwipe) + return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index e4e0952491..24be88c35b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -14,28 +14,30 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConf import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow /** * Main factory for creating [WalletStateHolder] * - * @property currentStateProvider current ui state provider - * @property clickCallbacks screen click callbacks + * @property currentStateProvider current ui state provider + * @param currentCardTypeResolverProvider current card type resolver + * @property clickIntents screen click intents */ internal class WalletStateFactory( private val currentStateProvider: Provider, currentCardTypeResolverProvider: Provider, - private val clickCallbacks: WalletClickCallbacks, + private val clickIntents: WalletClickIntents, ) { - private val skeletonConverter by lazy { WalletSkeletonStateConverter(clickCallbacks = clickCallbacks) } + private val skeletonConverter by lazy { WalletSkeletonStateConverter(clickIntents = clickIntents) } private val loadedTokensListConverter by lazy { WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, - clickCallbacks = clickCallbacks, + cardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, ) } @@ -53,7 +55,7 @@ internal class WalletStateFactory( ) } - fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickCallbacks::onBackClick) + fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickIntents::onBackClick) fun getSkeletonState(wallets: List): WalletStateHolder = skeletonConverter.convert(wallets) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index 2fe69a783c..b197bf76e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -1,19 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.utils +import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState import com.tangem.utils.converter.Converter internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, + private val cardTypeResolverProvider: Provider, + private val isLockedState: Boolean, private val isWalletContentHidden: Boolean, private val fiatCurrencyCode: String, private val fiatCurrencySymbol: String, ) : Converter { override fun convert(value: TokenList.FiatBalance): WalletCardState { - // TODO: [REDACTED_JIRA] + val additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolverProvider(), + isLocked = isLockedState, + ) return when (value) { is TokenList.FiatBalance.Loading -> with(currentState) { WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index 574cb11482..114cd724f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState.TokensListItemState import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -16,7 +16,7 @@ internal class TokenListToContentItemsConverter( isWalletContentHidden: Boolean, fiatCurrencyCode: String, fiatCurrencySymbol: String, - private val clickCallbacks: WalletClickCallbacks, + private val clickIntents: WalletClickIntents, ) : Converter { private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( @@ -33,7 +33,7 @@ internal class TokenListToContentItemsConverter( is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() }, onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) { - clickCallbacks::onOrganizeTokensClick + clickIntents::onOrganizeTokensClick } else { null }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 056bbb3d1d..06ec3d5b8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder @@ -8,23 +9,24 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.Mul import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList internal class TokenListToWalletStateConverter( private val currentStateProvider: Provider, + private val cardTypeResolverProvider: Provider, private val isWalletContentHidden: Boolean, private val fiatCurrencyCode: String, private val fiatCurrencySymbol: String, - clickCallbacks: WalletClickCallbacks, + clickIntents: WalletClickIntents, ) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, fiatCurrencyCode = fiatCurrencyCode, fiatCurrencySymbol = fiatCurrencySymbol, - clickCallbacks = clickCallbacks, + clickIntents = clickIntents, ) override fun convert(value: TokensListModel): WalletStateHolder { @@ -58,6 +60,8 @@ internal class TokenListToWalletStateConverter( val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] val converter = FiatBalanceToWalletCardConverter( currentState = selectedWalletCard, + isLockedState = this is WalletStateHolder.UnlockWalletContent, + cardTypeResolverProvider = cardTypeResolverProvider, isWalletContentHidden = isWalletContentHidden, fiatCurrencyCode = fiatCurrencyCode, fiatCurrencySymbol = fiatCurrencySymbol, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index f47c10bc5c..c92a9707a8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickCallbacks.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -internal interface WalletClickCallbacks { +internal interface WalletClickIntents { fun onBackClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 1bdd9dd004..aace48a09d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.flow * @property wasCardScannedCallback callback that check if card was scanned * @property isUserAlreadyRateAppCallback callback that check if card is user already rate app * @property isDemoCardCallback callback that check if card is demo - * @property clickCallbacks screen click callbacks + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ @@ -29,7 +29,7 @@ internal class WalletNotificationsListFactory( private val wasCardScannedCallback: suspend (String) -> Boolean, private val isUserAlreadyRateAppCallback: suspend () -> Boolean, private val isDemoCardCallback: (String) -> Boolean, - private val clickCallbacks: WalletClickCallbacks, + private val clickIntents: WalletClickIntents, ) { fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList?): Flow> { @@ -60,15 +60,15 @@ internal class WalletNotificationsListFactory( } if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) { - add(element = WalletNotification.BackupCard(onClick = clickCallbacks::onBackupCardClick)) + add(element = WalletNotification.BackupCard(onClick = clickIntents::onBackupCardClick)) } if (tokenList != null && tokenList.hasMissedDerivations()) { - add(element = WalletNotification.ScanCard(onClick = clickCallbacks::onScanCardClick)) + add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick)) } if (isUserAlreadyRateAppCallback()) { - add(element = WalletNotification.LikeTangemApp(onClick = clickCallbacks::onLikeTangemAppClick)) + add(element = WalletNotification.LikeTangemApp(onClick = clickIntents::onLikeTangemAppClick)) } }.toImmutableList(), ) @@ -94,13 +94,13 @@ internal class WalletNotificationsListFactory( if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) { add( element = WalletNotification.CriticalWarningAlreadySignedHashes( - onClick = clickCallbacks::onCriticalWarningAlreadySignedHashesClick, + onClick = clickIntents::onCriticalWarningAlreadySignedHashesClick, ), ) } else if (cardTypesResolver.hasWalletSignedHashes()) { add( element = WalletNotification.WarningAlreadySignedHashes( - onClick = clickCallbacks::onCloseWarningAlreadySignedHashesClick, + onClick = clickIntents::onCloseWarningAlreadySignedHashesClick, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 3b008c9023..b8f0fabe8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -52,7 +52,7 @@ internal class WalletViewModel @Inject constructor( private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val dispatchers: CoroutineDispatcherProvider, -) : ViewModel(), DefaultLifecycleObserver, WalletClickCallbacks { +) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() @@ -62,7 +62,7 @@ internal class WalletViewModel @Inject constructor( wasCardScannedCallback = getCardWasScannedUseCase::invoke, isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke, isDemoCardCallback = isDemoCardUseCase::invoke, - clickCallbacks = this, + clickIntents = this, ) private val stateFactory = WalletStateFactory( @@ -70,7 +70,7 @@ internal class WalletViewModel @Inject constructor( currentCardTypeResolverProvider = Provider { getCardTypeResolver(index = uiState.walletsListConfig.selectedWalletIndex) }, - clickCallbacks = this, + clickIntents = this, ) /** Screen state */ From 28d8d33aceb110ebace1526195c60ea9b8c9307f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Aug 2023 16:20:16 +0300 Subject: [PATCH 088/100] Updated on 2026-08-14 --- .../tap/common/extensions/WalletManager.kt | 2 +- .../di/domain/WalletManagersFacadeModule.kt | 29 ++++ .../tap/domain/extensions/Blockchain.kt | 35 ----- .../domain/tasks/product/ScanProductTask.kt | 2 +- .../DefaultWalletManagersRepository.kt | 2 +- .../utils/WalletDataOperations.kt | 2 +- .../demo/DemoOnboardingNoteMiddleware.kt | 2 +- .../note/redux/OnboardingNoteMiddleware.kt | 2 +- .../twins/redux/TwinCardsMiddleware.kt | 2 +- .../send/redux/middlewares/SendMiddleware.kt | 2 +- domain/legacy/build.gradle.kts | 4 +- .../domain/common/extensions/Blockchain.kt | 32 ++++ .../extensions/WalletManagerFactory.kt | 45 +----- .../DefaultWalletManagersFacade.kt | 140 ++++++++++++++++++ .../walletmanager/WalletManagerFacade.kt | 27 ++++ .../model/CryptoCurrencyAmount.kt | 15 ++ .../model/UpdateWalletManagerResult.kt | 17 +++ .../walletmanager/utils/SdkTokenConverter.kt | 18 +++ .../utils/UpdateWalletManagerResultFactory.kt | 87 +++++++++++ .../utils/WalletManagerFactory.kt | 47 ++++++ 20 files changed, 428 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt rename {app/src/main/java/com/tangem/tap/domain => domain/legacy/src/main/java/com/tangem/domain/common}/extensions/WalletManagerFactory.kt (67%) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 36dfe253b5..6665cab485 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.common.services.Result +import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.tap.common.TestActions import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.demo.isDemoCard diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt new file mode 100644 index 0000000000..2af3b68c76 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.di.domain + +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.walletmanager.DefaultWalletManagersFacade +import com.tangem.domain.walletmanager.WalletManagerFacade +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object WalletManagersFacadeModule { + + @Provides + @Singleton + fun provideWalletManagersFacade( + walletManagersStore: WalletManagersStore, + userWalletsStore: UserWalletsStore, + demoConfig: DemoConfig, + configManager: ConfigManager, + ): WalletManagerFacade { + return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, demoConfig, configManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt deleted file mode 100644 index 9adb6b24aa..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.domain.extensions - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.common.card.EllipticCurve -import java.math.BigDecimal - -@Suppress("MagicNumber") -fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { - return when (this) { - Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(1.5) else BigDecimal.ONE - Blockchain.XRP -> BigDecimal(10) - else -> null - } -} - -fun Blockchain.minimalAmount(): BigDecimal { - return 1.toBigDecimal().movePointLeft(decimals()) -} - -fun Blockchain.getPrimaryCurve(): EllipticCurve? { - return when { - getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { - EllipticCurve.Secp256k1 - } - getSupportedCurves().contains(EllipticCurve.Ed25519) -> { - EllipticCurve.Ed25519 - } - else -> { - null - } - } -} - -private const val NODL = "NODL" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index c3f23eb9b4..5bef94e017 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -20,6 +20,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TwinsHelper +import com.tangem.domain.common.extensions.getPrimaryCurve import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse @@ -30,7 +31,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError -import com.tangem.tap.domain.extensions.getPrimaryCurve import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.preferencesStorage import com.tangem.tap.scope diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 7a51a18e10..2fbd1e0624 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -9,13 +9,13 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.makeWalletManagerForApp import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index 85fd9d5c8e..c7eef5a974 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.address.AddressType import com.tangem.common.core.TangemError -import com.tangem.tap.domain.extensions.amountToCreateAccount +import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.demo.DemoHelper diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt index cb02be1422..ae1cf2fa2a 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard +import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 0006840098..711c5acc69 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -14,7 +15,6 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index b768c53091..023e3137c4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse @@ -19,7 +20,6 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index a76724c7c3..628e934435 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -16,6 +16,7 @@ import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.* @@ -31,7 +32,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.extensions.minimalAmount import com.tangem.tap.features.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.* diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 525d2c8504..a649d1ced1 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -9,7 +9,9 @@ dependencies { implementation(project(":core:utils")) implementation(project(":common")) implementation(project(":libs:auth")) - implementation(project(":domain:models")) + implementation(projects.domain.demo) + implementation(projects.domain.models) + implementation(projects.domain.tokens) implementation(projects.domain.wallets.models) /** Tangem libraries */ diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index da1221c137..c54218c39a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -1,6 +1,9 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.card.EllipticCurve +import java.math.BigDecimal @Suppress("ComplexMethod") fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { @@ -187,6 +190,35 @@ fun Blockchain.isSupportedInApp(): Boolean { return !excludedBlockchains.contains(this) } +fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { + return when (this) { + Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE + Blockchain.XRP -> BigDecimal.TEN + else -> null + } +} + +fun Blockchain.minimalAmount(): BigDecimal { + return 1.toBigDecimal().movePointLeft(decimals()) +} + +fun Blockchain.getPrimaryCurve(): EllipticCurve? { + return when { + getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + EllipticCurve.Secp256k1 + } + getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } + else -> { + null + } + } +} + +private const val NODL = "NODL" +private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 + private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt similarity index 67% rename from app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index 7253b1fccf..86ce14a4f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -1,17 +1,14 @@ -package com.tangem.tap.domain.extensions +package com.tangem.domain.common.extensions import com.tangem.blockchain.common.* import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.features.wallet.models.Currency fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, @@ -38,7 +35,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( curve = wallet.curve, ) } - scanResponse.card.isHdWalletAllowedByApp && seedKey != null && derivationParams != null -> { + scanResponse.card.settings.isHDWalletAllowed && seedKey != null && derivationParams != null -> { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] val derivationPath = when (derivationParams) { is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) @@ -64,23 +61,8 @@ fun WalletManagerFactory.makeWalletManagerForApp( } } -fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, - blockchainNetwork: BlockchainNetwork, -): WalletManager? { - return makeWalletManagerForApp( - scanResponse, - blockchain = blockchainNetwork.blockchain, - derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card), - ) -} - -private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? { - return derivationPath?.let { - DerivationParams.Custom( - DerivationPath(it), - ) - } ?: if (!card.settings.isHDWalletAllowed) { +private fun getDerivationParams(card: CardDTO): DerivationParams? { + return if (!card.settings.isHDWalletAllowed) { null } else if (card.useOldStyleDerivation) { DerivationParams.Default(DerivationStyle.LEGACY) @@ -89,30 +71,13 @@ private fun getDerivationParams(derivationPath: String?, card: CardDTO): Derivat } } -fun WalletManagerFactory.makeWalletManagerForApp(scanResponse: ScanResponse, currency: Currency): WalletManager? { - return makeWalletManagerForApp( - scanResponse, - blockchain = currency.blockchain, - derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card), - ) -} - -fun WalletManagerFactory.makeWalletManagersForApp( - scanResponse: ScanResponse, - blockchains: List, -): List { - return blockchains - .filter { it.isBlockchain() } - .mapNotNull { this.makeWalletManagerForApp(scanResponse, it) } -} - fun WalletManagerFactory.makePrimaryWalletManager(scanResponse: ScanResponse): WalletManager? { val blockchain = if (scanResponse.card.isTestCard) { scanResponse.cardTypesResolver.getBlockchain().getTestnetVersion() ?: return null } else { scanResponse.cardTypesResolver.getBlockchain() } - val derivationParams = getDerivationParams(null, scanResponse.card) + val derivationParams = getDerivationParams(scanResponse.card) return makeWalletManagerForApp( scanResponse = scanResponse, blockchain = blockchain, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt new file mode 100644 index 0000000000..59f6379645 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -0,0 +1,140 @@ +package com.tangem.domain.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.WalletManager +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.util.hasDerivation +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.walletmanager.utils.SdkTokenConverter +import com.tangem.domain.walletmanager.utils.UpdateWalletManagerResultFactory +import com.tangem.domain.walletmanager.utils.WalletManagerFactory +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import timber.log.Timber + +// FIXME: Move to its own module and make internal +@Deprecated("Inject the WalletManagerFacade interface using DI instead") +class DefaultWalletManagersFacade( + private val walletManagersStore: WalletManagersStore, + private val userWalletsStore: UserWalletsStore, + private val demoConfig: DemoConfig, + configManager: ConfigManager, +) : WalletManagerFacade { + + private val resultFactory by lazy { UpdateWalletManagerResultFactory() } + private val walletManagerFactory by lazy { WalletManagerFactory(configManager) } + private val sdkTokenConverter by lazy { SdkTokenConverter() } + + override suspend fun update( + userWalletId: UserWalletId, + networkId: Network.ID, + extraTokens: Set, + ): UpdateWalletManagerResult { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + val blockchain = Blockchain.fromId(networkId.value) + + return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) + } + + private suspend fun getAndUpdateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + extraTokens: Set, + ): UpdateWalletManagerResult { + val scanResponse = userWallet.scanResponse + val derivationPath = blockchain.derivationPath(scanResponse.card.derivationStyle) + + if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) { + Timber.e("Derivation missed for: $blockchain") + return UpdateWalletManagerResult.MissedDerivation + } + + val walletManager = getOrCreateWalletManager(userWallet, blockchain, derivationPath) + if (walletManager == null || blockchain == Blockchain.Unknown) { + Timber.e("Unable to get a wallet manager for blockchain: $blockchain") + return UpdateWalletManagerResult.Unreachable + } + + updateWalletManagerTokensIfNeeded(walletManager, extraTokens) + + return try { + if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + updateDemoWalletManager(walletManager, extraTokens) + } else { + updateWalletManager(walletManager) + } + } finally { + walletManagersStore.store(userWallet.walletId, walletManager) + } + } + + private fun updateDemoWalletManager( + walletManager: WalletManager, + tokens: Set, + ): UpdateWalletManagerResult { + val amount = demoConfig.getBalance(walletManager.wallet.blockchain) + walletManager.wallet.setAmount(amount) + + return resultFactory.getDemoResult(amount, tokens) + } + + private suspend fun updateWalletManager(walletManager: WalletManager): UpdateWalletManagerResult { + return try { + walletManager.update() + + resultFactory.getResult(walletManager) + } catch (e: BlockchainSdkError.AccountNotFound) { + resultFactory.getNoAccountResult(walletManager) + } catch (e: Throwable) { + Timber.e(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + + UpdateWalletManagerResult.Unreachable + } + } + + private suspend fun getOrCreateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? { + val userWalletId = userWallet.walletId + + var walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = derivationPath?.rawPath, + ) + + if (walletManager == null) { + walletManager = walletManagerFactory.createWalletManager( + scanResponse = userWallet.scanResponse, + blockchain = blockchain, + derivationPath = derivationPath, + ) ?: return null + + walletManagersStore.store(userWalletId, walletManager) + } + + return walletManager + } + + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { + if (tokens.isEmpty()) return + + val tokensToAdd = sdkTokenConverter + .convertList(tokens.toList()) + .filter { it !in walletManager.cardTokens } + + walletManager.addTokens(tokensToAdd) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt new file mode 100644 index 0000000000..bbfbd71dbd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.walletmanager + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId + +// TODO: Move to its own module +/** + * A facade for managing wallets. + */ +interface WalletManagerFacade { + + /** + * Updates the wallet manager associated with a user's wallet and network. + * + * @param userWalletId The ID of the user's wallet. + * @param networkId The network ID. + * @param extraTokens Additional tokens. + * @return The result of updating the wallet manager. + */ + suspend fun update( + userWalletId: UserWalletId, + networkId: Network.ID, + extraTokens: Set, + ): UpdateWalletManagerResult +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt new file mode 100644 index 0000000000..192be587e5 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletmanager.model + +import java.math.BigDecimal + +sealed class CryptoCurrencyAmount { + + abstract val value: BigDecimal + + data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() + + data class Token( + val tokenContractAddress: String, + override val value: BigDecimal, + ) : CryptoCurrencyAmount() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt new file mode 100644 index 0000000000..a516c03d0a --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletmanager.model + +import java.math.BigDecimal + +sealed class UpdateWalletManagerResult { + + object MissedDerivation : UpdateWalletManagerResult() + + object Unreachable : UpdateWalletManagerResult() + + data class Verified( + val tokensAmounts: Set, + val hasTransactionsInProgress: Boolean, // TODO: May be add recent transactions + ) : UpdateWalletManagerResult() + + data class NoAccount(val amountToCreateAccount: BigDecimal) : UpdateWalletManagerResult() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt new file mode 100644 index 0000000000..8fe4bdc174 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.Token as SdkToken + +internal class SdkTokenConverter : Converter { + + override fun convert(value: CryptoCurrency.Token): SdkToken { + return SdkToken( + id = value.id.value.takeUnless { value.isCustom }, + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + ) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt new file mode 100644 index 0000000000..bdf864f4fe --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -0,0 +1,87 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import timber.log.Timber +import java.math.BigDecimal + +internal class UpdateWalletManagerResultFactory { + + fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { + val hasNotConfirmedTransactions = walletManager.wallet + .recentTransactions + .any { it.status != TransactionStatus.Confirmed } + + val amounts = walletManager.wallet.amounts + + return UpdateWalletManagerResult.Verified( + tokensAmounts = getTokensAmounts(amounts.values.toSet()), + hasTransactionsInProgress = hasNotConfirmedTransactions, + ) + } + + fun getDemoResult(demoAmount: Amount, tokens: Set): UpdateWalletManagerResult.Verified { + return UpdateWalletManagerResult.Verified( + tokensAmounts = getDemoTokensAmounts(demoAmount, tokens), + hasTransactionsInProgress = false, + ) + } + + fun getNoAccountResult(walletManager: WalletManager): UpdateWalletManagerResult.NoAccount { + val wallet = walletManager.wallet + val blockchain = wallet.blockchain + val amountToCreateAccount = blockchain.amountToCreateAccount( + token = wallet.getTokens().firstOrNull(), + ) + + requireNotNull(amountToCreateAccount) { + "Unable to get required amount to create account for: $blockchain" + } + + return UpdateWalletManagerResult.NoAccount(amountToCreateAccount) + } + + private fun getTokensAmounts(amounts: Set): Set { + val mutableAmounts = hashSetOf() + + return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount) + } + + private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { + val amountValue = demoAmount.value ?: BigDecimal.ZERO + val demoAmounts = hashSetOf(CryptoCurrencyAmount.Coin(amountValue)) + + return tokens.mapTo(demoAmounts) { token -> + CryptoCurrencyAmount.Token(token.contractAddress, amountValue) + } + } + + private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? { + return when (val type = amount.type) { + is AmountType.Token -> CryptoCurrencyAmount.Token( + tokenContractAddress = type.token.contractAddress, + value = getAmountValue(amount) ?: return null, + ) + is AmountType.Coin -> CryptoCurrencyAmount.Coin( + value = getAmountValue(amount) ?: return null, + ) + is AmountType.Reserve -> null + } + } + + private fun getAmountValue(amount: Amount): BigDecimal? { + val value = amount.value + + if (value == null) { + Timber.e("Amount not found for currency: ${amount.currencySymbol}") + } + + return value + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt new file mode 100644 index 0000000000..f588b47702 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.makeWalletManagerForApp +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse + +internal class WalletManagerFactory( + private val configManager: ConfigManager, +) { + + private val sdkWalletManagerFactory by lazy { + WalletManagerFactory(configManager.config.blockchainSdkConfig) + } + + fun createWalletManager( + scanResponse: ScanResponse, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? { + val derivationParams = getDerivationParams(derivationPath, scanResponse.card) + + return sdkWalletManagerFactory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = blockchain, + derivationParams = derivationParams, + ) + } + + private fun getDerivationParams(derivationPath: DerivationPath?, card: CardDTO): DerivationParams? { + val derivationStyle = when { + !card.settings.isHDWalletAllowed -> return null + card.useOldStyleDerivation -> DerivationStyle.LEGACY + else -> DerivationStyle.NEW + } + + return if (derivationPath == null) { + DerivationParams.Default(derivationStyle) + } else { + DerivationParams.Custom(derivationPath) + } + } +} \ No newline at end of file From 1963a299a519fd5bd6720382eaf84462cb347853 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Aug 2023 14:54:06 +0300 Subject: [PATCH 089/100] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- settings.gradle.kts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 701091b1d4..edb6ad70d8 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,9 +80,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-289" +tangemBlockchainSdk = "develop-297" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-278" +tangemCardSdk = "develop-280" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem diff --git a/settings.gradle.kts b/settings.gradle.kts index 15622f6a47..18308d4085 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,18 @@ pluginManagement { includeBuild("plugins/configuration") } +val properties = java.util.Properties() +val propertiesFile = File(rootDir.absolutePath, "local.properties") +if (propertiesFile.exists()) { + properties.load(propertiesFile.inputStream()) + println("Authenticating user: " + properties.getProperty("gpr.user")) +} else { + println( + "local.properties not found, please create it next to build.gradle and set gpr.user and gpr.key (Create a GitHub package read only + non expiration token at https://github.com/settings/tokens)\n" + + "Or set GITHUB_ACTOR and GITHUB_TOKEN environment variables" + ) +} + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) @@ -17,6 +29,15 @@ dependencyResolutionManagement { mavenLocal() jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview maven("https://nexus.tangem-tech.com/repository/maven-releases/") + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") + credentials { + println(System.getenv("GITHUB_ACTOR")) + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + } maven("https://jitpack.io") maven("https://zendesk.jfrog.io/zendesk/repo") } From e0b5ca0c4d6e2d9bc885e8111676f12b48b79b3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 18:36:24 +0300 Subject: [PATCH 090/100] Updated on 2026-08-14 --- .../ApplyTokenListSortingUseCaseTest.kt | 2 +- .../tokens/GetPrimaryCurrencyUseCaseTest.kt | 167 ++++++++++++++++++ .../domain/tokens/GetTokenListUseCaseTest.kt | 8 +- .../tokens/repository/MockTokensRepository.kt | 4 +- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index ab5b194a5b..0bdf923b6b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -193,6 +193,6 @@ internal class ApplyTokenListSortingUseCaseTest { sortTokensResult: Either = Unit.right(), tokens: Flow>> = flowOf(MockTokens.tokens.right()), ): MockTokensRepository { - return MockTokensRepository(sortTokensResult, tokens, emptyFlow(), emptyFlow()) + return MockTokensRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow()) } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt new file mode 100644 index 0000000000..95026c6926 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt @@ -0,0 +1,167 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockQuotes +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.mock.MockTokensStates +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.repository.MockNetworksRepository +import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockTokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class GetPrimaryCurrencyUseCaseTest { + + private val dispatchers = TestingCoroutineDispatcherProvider() + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when all data received then token should be received`() = runTest { + // Given + val expectedResult = MockTokensStates.loadedTokensStates.first().right() + + val useCase = getUseCase() + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when token getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses flow is empty then error should be received`() = runTest { + val expectedResult = TokenError.UnableToCreateToken.left() + + val useCase = getUseCase(statuses = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes flow is empty then error should be received`() = runTest { + val expectedResult = TokenError.UnableToCreateToken.left() + + val useCase = getUseCase(quotes = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes are empty and statuses are verified then loading token should be received`() = runTest { + val expectedResult = MockTokensStates.tokenState1 + .copy(value = CryptoCurrencyStatus.Loading) + .right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + quotes = flowOf(emptySet().right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes are loaded and statuses are empty then loading token should be received`() = runTest { + val expectedResult = MockTokensStates.tokenState1 + .copy(value = CryptoCurrencyStatus.Loading) + .right() + + val useCase = getUseCase( + statuses = flowOf(emptySet().right()), + quotes = flowOf(MockQuotes.quotes.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase( + token: Either = MockTokens.token1.right(), + quotes: Flow>> = flowOf(MockQuotes.quotes.right()), + statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + ) = GetPrimaryCurrencyUseCase( + dispatchers = dispatchers, + tokensRepository = MockTokensRepository( + sortTokensResult = Unit.right(), + token = token, + tokens = flowOf(), + isGrouped = flowOf(), + isSortedByBalance = flowOf(), + ), + quotesRepository = MockQuotesRepository(quotes), + networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index a1ff4474c9..57101b51f6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -326,7 +326,13 @@ internal class GetTokenListUseCaseTest { isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), ) = GetTokenListUseCase( dispatchers = dispatchers, - tokensRepository = MockTokensRepository(Unit.right(), tokens, isGrouped, isSortedByBalance), + tokensRepository = MockTokensRepository( + sortTokensResult = Unit.right(), + token = MockTokens.token1.right(), + tokens = tokens, + isGrouped = isGrouped, + isSortedByBalance = isSortedByBalance, + ), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(networks, statuses), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index 8e66695d8f..ad5d4e0851 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -3,7 +3,6 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -11,6 +10,7 @@ import kotlinx.coroutines.flow.map internal class MockTokensRepository( private val sortTokensResult: Either, + private val token: Either, private val tokens: Flow>>, private val isGrouped: Flow>, private val isSortedByBalance: Flow>, @@ -39,7 +39,7 @@ internal class MockTokensRepository( } override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { - return MockTokens.token1 + return token.getOrElse { e -> throw e } } override fun getMultiCurrencyWalletCurrencies( From ef438c3cd391d0d9a20396c9a13b22188fd1a314 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jul 2023 10:30:50 +0300 Subject: [PATCH 091/100] Updated on 2026-08-14 --- .../organizetokens/OrganizeTokensViewModel.kt | 10 +++++++- .../router/DefaultWalletRouter.kt | 23 +++++++++++------- .../presentation/router/InnerWalletRouter.kt | 3 ++- .../wallet/presentation/router/WalletRoute.kt | 24 +++++++++++++++++++ .../presentation/router/WalletScreens.kt | 10 -------- .../wallet/viewmodels/WalletViewModel.kt | 7 +++++- 6 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index e81b764058..52f391935b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -3,13 +3,16 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.DragConfig import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.HeaderConfig import com.tangem.feature.wallet.presentation.organizetokens.utils.* import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.router.WalletRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers @@ -20,12 +23,17 @@ import kotlin.properties.Delegates // FIXME: Implemented with preview data @HiltViewModel -internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() { +internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: SavedStateHandle) : ViewModel() { @Volatile private var movingItem: DraggableItem? = null var router: InnerWalletRouter by Delegates.notNull() + val userWalletId: UserWalletId by lazy { + val userWalletIdValue: String = checkNotNull(savedStateHandle[WalletRoute.userWalletIdKey]) + + UserWalletId(userWalletIdValue) + } var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState()) private set diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 85fb11d43b..642d59b17b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.router -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -9,12 +8,15 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel @@ -36,20 +38,23 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation NavHost( navController = rememberNavController().apply { navController = this }, - startDestination = WalletScreens.WALLET.name, + startDestination = WalletRoute.Wallet.route, ) { - composable(WalletScreens.WALLET.name) { + composable(WalletRoute.Wallet.route) { val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) WalletScreen(state = viewModel.uiState) } - composable(WalletScreens.ORGANIZE_TOKENS.name) { - BackHandler(onBack = ::popBackStack) - + composable( + WalletRoute.OrganizeTokens.route, + arguments = listOf(navArgument(WalletRoute.userWalletIdKey) { type = NavType.StringType }), + ) { val viewModel: OrganizeTokensViewModel = hiltViewModel() - .apply { router = this@DefaultWalletRouter } + .apply { + router = this@DefaultWalletRouter + } OrganizeTokensScreen( modifier = Modifier.systemBarsPadding(), @@ -73,8 +78,8 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation } } - override fun openOrganizeTokensScreen() { - navController.navigate(route = WalletScreens.ORGANIZE_TOKENS.name) + override fun openOrganizeTokensScreen(userWalletId: UserWalletId) { + navController.navigate(WalletRoute.OrganizeTokens.createRoute(userWalletId)) } override fun openDetailsScreen() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 866bbc9c3d..ef0a6a93b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.fragment.app.FragmentManager +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.wallet.navigation.WalletRouter /** @@ -29,7 +30,7 @@ internal interface InnerWalletRouter : WalletRouter { fun popBackStack() /** Open organize tokens screen */ - fun openOrganizeTokensScreen() + fun openOrganizeTokensScreen(userWalletId: UserWalletId) /** Open details screen */ fun openDetailsScreen() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt new file mode 100644 index 0000000000..2cb8a4a91c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.router + +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Wallet feature screens + * + * @property route route string representation + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletRoute(val route: String) { + + object Wallet : WalletRoute(route = "wallet") + + object OrganizeTokens : WalletRoute(route = "wallet/{$userWalletIdKey}/organize_tokens") { + + fun createRoute(userWalletId: UserWalletId) = "wallet/${userWalletId.stringValue}/organize_tokens" + } + + companion object { + const val userWalletIdKey = "userWalletId" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt deleted file mode 100644 index 0a74b766aa..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.feature.wallet.presentation.router - -/** - * Wallet feature screens - * -[REDACTED_AUTHOR] - */ -enum class WalletScreens { - WALLET, ORGANIZE_TOKENS -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index b8f0fabe8a..7dbc2e6af9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -243,5 +243,10 @@ internal class WalletViewModel @Inject constructor( updateContentItems(index = uiState.walletsListConfig.selectedWalletIndex, isRefreshing = true) } - override fun onOrganizeTokensClick() = router.openOrganizeTokensScreen() + override fun onOrganizeTokensClick() { + val index = uiState.walletsListConfig.selectedWalletIndex + val walletId = uiState.walletsListConfig.wallets[index].id + + router.openOrganizeTokensScreen(walletId) + } } \ No newline at end of file From 44335fbd8d2a60f0d8fdd8d9450bec9340748675 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Aug 2023 17:03:35 +0300 Subject: [PATCH 092/100] Updated on 2026-08-14 --- .../tangem/tap/di/domain/WalletManagersFacadeModule.kt | 8 +++----- .../domain/walletmanager/DefaultWalletManagersFacade.kt | 4 ++-- .../{WalletManagerFacade.kt => WalletManagersFacade.kt} | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) rename domain/legacy/src/main/java/com/tangem/domain/walletmanager/{WalletManagerFacade.kt => WalletManagersFacade.kt} (96%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 2af3b68c76..1c4bc06044 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -3,9 +3,8 @@ package com.tangem.tap.di.domain import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.walletmanager.DefaultWalletManagersFacade -import com.tangem.domain.walletmanager.WalletManagerFacade +import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,9 +20,8 @@ internal object WalletManagersFacadeModule { fun provideWalletManagersFacade( walletManagersStore: WalletManagersStore, userWalletsStore: UserWalletsStore, - demoConfig: DemoConfig, configManager: ConfigManager, - ): WalletManagerFacade { - return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, demoConfig, configManager) + ): WalletManagersFacade { + return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, configManager) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 59f6379645..dabbd65f90 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -25,10 +25,10 @@ import timber.log.Timber class DefaultWalletManagersFacade( private val walletManagersStore: WalletManagersStore, private val userWalletsStore: UserWalletsStore, - private val demoConfig: DemoConfig, configManager: ConfigManager, -) : WalletManagerFacade { +) : WalletManagersFacade { + private val demoConfig by lazy { DemoConfig() } private val resultFactory by lazy { UpdateWalletManagerResultFactory() } private val walletManagerFactory by lazy { WalletManagerFactory(configManager) } private val sdkTokenConverter by lazy { SdkTokenConverter() } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt similarity index 96% rename from domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt rename to domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index bbfbd71dbd..4c4fd8d26d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagerFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -9,7 +9,7 @@ import com.tangem.domain.wallets.models.UserWalletId /** * A facade for managing wallets. */ -interface WalletManagerFacade { +interface WalletManagersFacade { /** * Updates the wallet manager associated with a user's wallet and network. From 3d887a5574dc9910447bf2b9e4db3c8ced276639 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Aug 2023 15:20:47 +0400 Subject: [PATCH 093/100] Updated on 2026-08-14 --- .../intentHandler/handlers/BackgroundScanIntentHandler.kt | 5 +++-- .../intentHandler/handlers/SellCurrencyIntentHandler.kt | 3 ++- .../intentHandler/handlers/WalletConnectLinkIntentHandler.kt | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 18a51c222f..df5b3759d8 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.welcome.redux.WelcomeAction @@ -41,10 +42,10 @@ class BackgroundScanIntentHandler( // TODO: Remove delay after [REDACTED_JIRA] scope.launch { delay(timeMillis = 200) - store.dispatch(WelcomeAction.ProceedWithCard) + store.dispatchWithMain(WelcomeAction.ProceedWithCard) } } else { - store.dispatch(HomeAction.ReadCard()) + store.dispatchWithMain(HomeAction.ReadCard()) } return true diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 3fc6c318d6..9d28a45db6 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store @@ -20,7 +21,7 @@ class SellCurrencyIntentHandler : IntentHandler { val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatch( + store.dispatchWithMain( WalletAction.TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index e61d0b74d7..79e4774682 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -25,7 +26,7 @@ class WalletConnectLinkIntentHandler : IntentHandler { return if (wcUri == null) { false } else { - store.dispatch(WalletConnectAction.HandleDeepLink(wcUri)) + store.dispatchWithMain(WalletConnectAction.HandleDeepLink(wcUri)) true } } From 06b13f2227ae1db3986faffc3810f32cdd8a231a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Jul 2023 18:59:35 +0800 Subject: [PATCH 094/100] Updated on 2026-08-14 --- .../transactions/TransactionState.kt | 20 +- core/utils/build.gradle.kts | 10 +- .../utils/CryptoAddressFormatExtensions.kt | 14 ++ ...s.kt => CryptoCurrencyFormatExtensions.kt} | 0 .../com/tangem/utils/extensions/DateTime.kt | 8 + .../data/txhistory/mock/MockTxHistoryItems.kt | 12 +- features/wallet/impl/build.gradle.kts | 2 +- .../factory/WalletLoadedTxHistoryConverter.kt | 130 ----------- .../WalletLoadingTxHistoryConverter.kt | 103 --------- .../state/factory/WalletStateFactory.kt | 4 + .../WalletLoadedTxHistoryConverter.kt | 95 ++++++++ .../WalletLoadingTxHistoryConverter.kt | 102 +++++++++ .../WalletTxHistoryItemFlowConverter.kt | 216 ++++++++++++++++++ .../wallet/viewmodels/WalletClickIntents.kt | 6 + .../wallet/viewmodels/WalletViewModel.kt | 12 + 15 files changed, 491 insertions(+), 243 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt rename core/utils/src/main/java/com/tangem/utils/{FormatExtensions.kt => CryptoCurrencyFormatExtensions.kt} (100%) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt index f0502a4a21..b5b6adaaba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt @@ -18,7 +18,25 @@ sealed interface TransactionState { open val address: String, open val amount: String, open val timestamp: String, - ) : TransactionState + ) : TransactionState { + + fun copySealed( + address: String = this.address, + amount: String = this.amount, + timestamp: String = this.timestamp, + ): Content { + return when (this) { + is Approved -> copy(address, amount, timestamp) + is Receive -> copy(address, amount, timestamp) + is Send -> copy(address, amount, timestamp) + is Swapped -> copy(address, amount, timestamp) + is Approving -> copy(address, amount, timestamp) + is Receiving -> copy(address, amount, timestamp) + is Sending -> copy(address, amount, timestamp) + is Swapping -> copy(address, amount, timestamp) + } + } + } /** * Content state for processed transaction diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 1e8960ce77..bf7c86b763 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -6,10 +6,16 @@ plugins { dependencies { - /** DI */ + // region DI implementation(deps.hilt.core) kapt(deps.hilt.kapt) + // endregion - /** Coroutines */ + // region Coroutines implementation(deps.kotlin.coroutines) + // endregion + + // region Time dependencies + implementation(deps.jodatime) + // endregion } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt new file mode 100644 index 0000000000..f2b5b09fce --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt @@ -0,0 +1,14 @@ +package com.tangem.utils + +/** + * Convert address to brief format. Example, 33BddS...ga2B. + * If [this.length] is less than a sum of [startCharsCount] and [endCharsCount], return [this]. + */ +fun String.toBriefAddressFormat(startCharsCount: Int = 6, endCharsCount: Int = 4): String { + return if (startCharsCount + endCharsCount < length) { + substring(startIndex = 0, endIndex = startCharsCount) + "..." + + substring(startIndex = length - endCharsCount, endIndex = length) + } else { + this + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/FormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt similarity index 100% rename from core/utils/src/main/java/com/tangem/utils/FormatExtensions.kt rename to core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt b/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt new file mode 100644 index 0000000000..3fad6d57d7 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.extensions + +import org.joda.time.DateTime +import org.joda.time.LocalDate + +fun DateTime.isToday(): Boolean = LocalDate.now().equals(LocalDate(this)) + +fun DateTime.isYesterday(): Boolean = LocalDate.now().minusDays(1).equals(LocalDate(this)) \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt index 4f7cbec344..583fbaa5ee 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt @@ -7,7 +7,7 @@ internal object MockTxHistoryItems { private val txHistoryItem1 = TxHistoryItem( txHash = "noster", - timestamp = 1689930746, + timestamp = System.currentTimeMillis(), direction = TxHistoryItem.TransactionDirection.Incoming("address"), status = TxHistoryItem.TxStatus.Confirmed, type = TxHistoryItem.TransactionType.Transfer, @@ -16,7 +16,7 @@ internal object MockTxHistoryItems { private val txHistoryItem2 = TxHistoryItem( txHash = "noster", - timestamp = 1689844346, + timestamp = 1689844346000, direction = TxHistoryItem.TransactionDirection.Incoming("address2"), status = TxHistoryItem.TxStatus.Unconfirmed, type = TxHistoryItem.TransactionType.Transfer, @@ -25,7 +25,7 @@ internal object MockTxHistoryItems { private val txHistoryItem3 = TxHistoryItem( txHash = "noster", - timestamp = 1689757946, + timestamp = 1689757946000, direction = TxHistoryItem.TransactionDirection.Outgoing("address3"), status = TxHistoryItem.TxStatus.Confirmed, type = TxHistoryItem.TransactionType.Transfer, @@ -34,7 +34,7 @@ internal object MockTxHistoryItems { private val txHistoryItem4 = TxHistoryItem( txHash = "noster", - timestamp = 1689671546, + timestamp = 1689671546000, direction = TxHistoryItem.TransactionDirection.Incoming("address4"), status = TxHistoryItem.TxStatus.Confirmed, type = TxHistoryItem.TransactionType.Transfer, @@ -43,7 +43,7 @@ internal object MockTxHistoryItems { private val txHistoryItem5 = TxHistoryItem( txHash = "noster", - timestamp = 1689585146, + timestamp = 1689585146000, direction = TxHistoryItem.TransactionDirection.Outgoing("address5"), status = TxHistoryItem.TxStatus.Unconfirmed, type = TxHistoryItem.TransactionType.Transfer, @@ -52,7 +52,7 @@ internal object MockTxHistoryItems { private val txHistoryItem6 = TxHistoryItem( txHash = "noster", - timestamp = 1689585146, + timestamp = 1689585146000, direction = TxHistoryItem.TransactionDirection.Incoming("address6"), status = TxHistoryItem.TxStatus.Confirmed, type = TxHistoryItem.TransactionType.Transfer, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 25b400217f..606d847a63 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -28,10 +28,10 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) + implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) - implementation(deps.arrow.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt deleted file mode 100644 index 1d8fa010d8..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTxHistoryConverter.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.factory - -import androidx.paging.PagingData -import androidx.paging.map -import arrow.core.Either -import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.txhistory.error.TxHistoryListError -import com.tangem.domain.txhistory.model.TxHistoryItem -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** - * Converter from loaded tx history to [WalletTxHistoryState] - * - * @property currentStateProvider current state provider - * @property currentCardTypeResolverProvider current card type resolver provider - * -[REDACTED_AUTHOR] - */ -internal class WalletLoadedTxHistoryConverter( - private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, -) : Converter>>, WalletStateHolder> { - - override fun convert(value: Either>>): WalletStateHolder { - return value.fold(ifLeft = { convertError() }, ifRight = ::convert) - } - - private fun convertError(): WalletStateHolder { - val state = currentStateProvider() - return WalletStateHolder.SingleCurrencyContent( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig, - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - notifications = state.notifications, - bottomSheet = state.bottomSheet, - buttons = persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - .map(WalletManageButton::config) - .toImmutableList(), - marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = currentCardTypeResolverProvider().getBlockchain().currency, - ), - // TODO: [REDACTED_JIRA] - txHistoryState = WalletTxHistoryState.Error(onReloadClick = {}), - ) - } - - private fun convert(items: Flow>): WalletStateHolder { - val state = currentStateProvider() - return WalletStateHolder.SingleCurrencyContent( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig, - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - notifications = state.notifications, - bottomSheet = state.bottomSheet, - buttons = persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - .map(WalletManageButton::config) - .toImmutableList(), - marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = currentCardTypeResolverProvider().getBlockchain().currency, - ), - txHistoryState = createContentTxHistory(items), - ) - } - - private fun createContentTxHistory(items: Flow>): WalletTxHistoryState { - return WalletTxHistoryState.Content( - items = items.map { pagingData -> - pagingData.map { item -> - TxHistoryItemState.Transaction( - state = when (val direction = item.direction) { - is TxHistoryItem.TransactionDirection.Incoming -> { - when (item.status) { - TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( - address = direction.from, - amount = item.amount.toPlainString(), - timestamp = item.timestamp.toString(), - ) - TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( - address = direction.from, - amount = item.amount.toPlainString(), - timestamp = item.timestamp.toString(), - ) - } - } - is TxHistoryItem.TransactionDirection.Outgoing -> { - when (item.status) { - TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( - address = direction.to, - amount = item.amount.toPlainString(), - timestamp = item.timestamp.toString(), - ) - TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( - address = direction.to, - amount = item.amount.toPlainString(), - timestamp = item.timestamp.toString(), - ) - } - } - }, - ) - } - }, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt deleted file mode 100644 index b24b26dd5a..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadingTxHistoryConverter.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.factory - -import androidx.paging.PagingData -import arrow.core.Either -import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.txhistory.error.TxHistoryStateError -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.flowOf - -/** - * Converter from loading tx history to [WalletTxHistoryState] - * - * @property currentStateProvider current state provider - * @property currentCardTypeResolverProvider current card type resolver provider - * -[REDACTED_AUTHOR] - */ -internal class WalletLoadingTxHistoryConverter( - private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, -) : Converter, WalletStateHolder> { - - override fun convert(value: Either): WalletStateHolder { - return value.fold(ifLeft = ::convertError, ifRight = ::convert) - } - - private fun convert(value: Int): WalletStateHolder { - val state = currentStateProvider() - - return WalletStateHolder.SingleCurrencyContent( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig, - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - notifications = state.notifications, - bottomSheet = state.bottomSheet, - buttons = persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - .map(WalletManageButton::config) - .toImmutableList(), - marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = currentCardTypeResolverProvider().getBlockchain().currency, - ), - txHistoryState = createLoadingTxHistory(itemCount = value), - ) - } - - private fun createLoadingTxHistory(itemCount: Int): WalletTxHistoryState { - return WalletTxHistoryState.Content( - items = flowOf( - value = PagingData.from( - data = buildList( - capacity = itemCount, - builderAction = { - add( - element = WalletTxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Loading, - ), - ) - }, - ), - ), - ), - ) - } - - private fun convertError(error: TxHistoryStateError): WalletStateHolder { - val state = currentStateProvider() - - return WalletStateHolder.SingleCurrencyContent( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig, - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - notifications = state.notifications, - bottomSheet = state.bottomSheet, - buttons = WalletPreviewData.singleWalletScreenState.buttons, - marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = currentCardTypeResolverProvider().getBlockchain().currency, - ), - // TODO: [REDACTED_JIRA] - txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> WalletTxHistoryState.Empty(onBuyClick = {}) - is TxHistoryStateError.DataError -> WalletTxHistoryState.Error(onReloadClick = {}) - is TxHistoryStateError.TxHistoryNotImplemented -> WalletTxHistoryState.NotSupported(onExploreClick = {}) - }, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 24be88c35b..453c182e49 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -14,6 +14,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConf import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter +import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -45,6 +47,7 @@ internal class WalletStateFactory( WalletLoadingTxHistoryConverter( currentStateProvider = currentStateProvider, currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, ) } @@ -52,6 +55,7 @@ internal class WalletStateFactory( WalletLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..5ed89bf2e9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow + +/** + * Converter from loaded tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter>>, WalletStateHolder> { + + private val walletTxHistoryItemFlowConverter by lazy { + WalletTxHistoryItemFlowConverter( + blockchain = currentCardTypeResolverProvider().getBlockchain(), + clickIntents = clickIntents, + ) + } + + override fun convert(value: Either>>): WalletStateHolder { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryListError): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + + private fun convert(items: Flow>): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), + ) + } + + private fun WalletStateHolder.copySingleCurrencyContent( + txHistoryState: WalletTxHistoryState, + ): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + buttons = getButtons(), + marketPriceBlockState = getLoadingMarketPriceBlockState(), + txHistoryState = txHistoryState, + ) + } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList() + } + + private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { + return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().getBlockchain().currency) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..da2936afcc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -0,0 +1,102 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flowOf + +/** + * Converter from loading tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter, WalletStateHolder> { + + override fun convert(value: Either): WalletStateHolder { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convert(value: Int): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = WalletTxHistoryState.Content( + items = flowOf( + value = PagingData.from( + data = buildList(capacity = value) { + add(WalletTxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading)) + }, + ), + ), + ), + ) + } + + private fun convertError(error: TxHistoryStateError): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + WalletTxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + WalletTxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } + + private fun WalletStateHolder.copySingleCurrencyContent( + txHistoryState: WalletTxHistoryState, + ): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + buttons = getButtons(), + marketPriceBlockState = getLoadingMarketPriceBlockState(), + txHistoryState = txHistoryState, + ) + } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList() + } + + private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { + return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().getBlockchain().currency) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..2fe01c3e0e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -0,0 +1,216 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +/** + * Convert from [Flow] of [TxHistoryItem] to [WalletTxHistoryState] + * + * @property blockchain blockchain of transactions history + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletTxHistoryItemFlowConverter( + private val blockchain: Blockchain, + private val clickIntents: WalletClickIntents, +) : Converter>, WalletTxHistoryState> { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: Flow>): WalletTxHistoryState { + return WalletTxHistoryState.Content( + items = value + .map { pagingData -> + pagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + }, + ) + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return when (item.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(item, direction, blockchain) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(item, direction, blockchain) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + blockchain: Blockchain, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + blockchain: Blockchain, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(blockchain: Blockchain): String { + return toFormattedCurrencyString(currency = blockchain.currency, decimals = blockchain.decimals()) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + txHistoryItemState.copy( + state = txHistoryItemState.state.copySealed( + timestamp = txHistoryItemState.state.timestamp.toTimeFormat(), + ), + ) + } else { + txHistoryItemState + } + } + } + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestamp.toString() + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + requireNotNull(this.state.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date by [dateFormatter] + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } + + private fun String.toTimeFormat(): String { + return timeFormatter.print( + DateTime(this.toLong(), DateTimeZone.getDefault()), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index c92a9707a8..503b697419 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -25,4 +25,10 @@ internal interface WalletClickIntents { fun onRefreshSwipe() fun onOrganizeTokensClick() + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 7dbc2e6af9..af2d2daddd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -249,4 +249,16 @@ internal class WalletViewModel @Inject constructor( router.openOrganizeTokensScreen(walletId) } + + override fun onBuyClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onReloadClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onExploreClick() { + // TODO: [REDACTED_JIRA] + } } \ No newline at end of file From ff985dec4eafa898f5b6825b362a529e4466b839 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Aug 2023 22:59:39 +0800 Subject: [PATCH 095/100] Updated on 2026-08-14 --- .../buttons/HorizontalActionChips.kt | 15 ++-- .../buttons/actions/ActionButtonConfig.kt | 3 +- .../ui/components/buttons/actions/Actions.kt | 18 +++- .../empty/EmptyTransactionBlock.kt | 34 +++----- .../empty/EmptyTransactionsBlockState.kt | 62 ++++++++------ .../tokendetails/TokenDetailsPreviewData.kt | 9 +- .../organizetokens/OrganizeTokensScreen.kt | 5 +- .../router/DefaultWalletRouter.kt | 4 + .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/WalletManageButton.kt | 11 +-- .../state/content/WalletTxHistoryState.kt | 40 ++++----- .../presentation/wallet/ui/WalletScreen.kt | 55 +----------- .../multicurrency/MultiCurrencyContent.kt | 31 +++++++ .../multicurrency/OrganizeTokensButton.kt | 4 +- .../singlecurrency/SingleCurrencyContent.kt | 85 +++++++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 26 +++--- 16 files changed, 246 insertions(+), 159 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index de7706a43d..c778c6c0a4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -30,11 +31,7 @@ fun HorizontalActionChips( verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, ) { - items( - items = buttons, - key = { it.text }, - itemContent = { ActionButton(config = it) }, - ) + items(items = buttons, itemContent = { ActionButton(config = it) }) } } @@ -61,22 +58,22 @@ private fun Preview_HorizontalActionChips_Dark( private class ActionButtonConfigProvider : CollectionPreviewParameterProvider( collection = persistentListOf( ActionButtonConfig( - text = "Buy", + text = TextReference.Str(value = "Buy"), iconResId = R.drawable.ic_plus_24, onClick = {}, ), ActionButtonConfig( - text = "Send", + text = TextReference.Str(value = "Send"), iconResId = R.drawable.ic_arrow_up_24, onClick = {}, ), ActionButtonConfig( - text = "Receive", + text = TextReference.Str(value = "Receive"), iconResId = R.drawable.ic_arrow_down_24, onClick = {}, ), ActionButtonConfig( - text = "Exchange", + text = TextReference.Str(value = "Exchange"), iconResId = R.drawable.ic_exchange_vertical_24, onClick = {}, ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index 3e69942c7f..ae19be321b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.buttons.actions import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference /** * Action button config @@ -13,7 +14,7 @@ import androidx.annotation.DrawableRes [REDACTED_AUTHOR] */ data class ActionButtonConfig( - val text: String, + val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val enabled: Boolean = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 068172b71e..af38dad59f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.buttons.common.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme /** @@ -103,7 +105,7 @@ private fun Button( SpacerW8() Text( - text = config.text, + text = config.text.resolveReference(), color = if (config.enabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled, overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -146,7 +148,17 @@ private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::cla private class ActionStateProvider : CollectionPreviewParameterProvider( collection = listOf( - ActionButtonConfig(text = "Enabled", iconResId = R.drawable.ic_arrow_up_24, enabled = true, onClick = {}), - ActionButtonConfig(text = "Disabled", iconResId = R.drawable.ic_arrow_down_24, enabled = false, onClick = {}), + ActionButtonConfig( + text = TextReference.Str(value = "Enabled"), + iconResId = R.drawable.ic_arrow_up_24, + enabled = true, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Disabled"), + iconResId = R.drawable.ic_arrow_down_24, + enabled = false, + onClick = {}, + ), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt index 4bb5a8f0b1..4045bf505e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -14,11 +14,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.buttons.actions.ActionButton -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.R +/** + * Placeholder for transaction's block without content + * + * @param state component state + * @param modifier modifier + */ @Composable fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { Column( @@ -34,6 +38,7 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier painter = painterResource(id = state.iconRes), contentDescription = null, ) + Text( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing32), textAlign = TextAlign.Center, @@ -41,6 +46,7 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) + ActionButton(config = state.actionButtonConfig) } } @@ -67,26 +73,8 @@ private fun EmptyTransactionBlock_Dark( private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - EmptyTransactionsBlockState.Empty( - actionButtonConfig = ActionButtonConfig( - text = "Buy", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - ), - EmptyTransactionsBlockState.FailedToLoad( - actionButtonConfig = ActionButtonConfig( - text = "Reload", - iconResId = R.drawable.ic_refresh_24, - onClick = {}, - ), - ), - EmptyTransactionsBlockState.NotImplemented( - actionButtonConfig = ActionButtonConfig( - text = "Explore transaction history", - iconResId = R.drawable.ic_arrow_top_right_24, - onClick = {}, - ), - ), + EmptyTransactionsBlockState.Empty(onClick = {}), + EmptyTransactionsBlockState.FailedToLoad(onClick = {}), + EmptyTransactionsBlockState.NotImplemented(onClick = {}), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt index bb26676865..d4c4dad238 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt @@ -1,37 +1,45 @@ package com.tangem.core.ui.components.transactions.empty -import androidx.annotation.DrawableRes import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference -sealed class EmptyTransactionsBlockState { +sealed class EmptyTransactionsBlockState( + val iconRes: Int, + val text: TextReference, + val actionButtonConfig: ActionButtonConfig, +) { - @get:DrawableRes - abstract val iconRes: Int + class FailedToLoad(onClick: () -> Unit) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_reload), + iconResId = R.drawable.ic_refresh_24, + onClick = onClick, + enabled = true, + ), + iconRes = R.drawable.ic_alert_history_64, + text = TextReference.Res(R.string.transaction_history_error_failed_to_load), + ) - abstract val text: TextReference + class Empty(onClick: (() -> Unit)?) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_buy), + iconResId = R.drawable.ic_plus_24, + onClick = onClick ?: {}, + enabled = onClick != null, + ), + iconRes = R.drawable.img_coin_64, + text = TextReference.Res(R.string.transaction_history_empty_transactions), + ) - abstract val actionButtonConfig: ActionButtonConfig - - data class FailedToLoad( - override val actionButtonConfig: ActionButtonConfig, - ) : EmptyTransactionsBlockState() { - override val iconRes: Int = R.drawable.ic_alert_history_64 - override val text: TextReference = TextReference.Res(R.string.transaction_history_error_failed_to_load) - } - - data class Empty( - override val actionButtonConfig: ActionButtonConfig, - ) : EmptyTransactionsBlockState() { - override val iconRes: Int = R.drawable.img_coin_64 - override val text: TextReference = TextReference.Res(R.string.transaction_history_empty_transactions) - } - - data class NotImplemented( - override val actionButtonConfig: ActionButtonConfig, - ) : EmptyTransactionsBlockState() { - override val iconRes: Int = R.drawable.ic_compass_64 - override val text: TextReference = TextReference.Res(R.string.transaction_history_not_supported_description) - } + class NotImplemented(onClick: () -> Unit) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_explore_transaction_history), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = onClick, + enabled = true, + ), + iconRes = R.drawable.ic_compass_64, + text = TextReference.Res(R.string.transaction_history_not_supported_description), + ) } \ No newline at end of file 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 5c20a6664d..47cef6133f 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 @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -42,22 +43,22 @@ internal object TokenDetailsPreviewData { private val actionButtons = persistentListOf( ActionButtonConfig( - text = "Buy", + text = TextReference.Str(value = "Buy"), iconResId = R.drawable.ic_plus_24, onClick = {}, ), ActionButtonConfig( - text = "Send", + text = TextReference.Str(value = "Send"), iconResId = R.drawable.ic_arrow_up_24, onClick = {}, ), ActionButtonConfig( - text = "Receive", + text = TextReference.Str(value = "Receive"), iconResId = R.drawable.ic_arrow_down_24, onClick = {}, ), ActionButtonConfig( - text = "Exchange", + text = TextReference.Str(value = "Exchange"), iconResId = R.drawable.ic_exchange_vertical_24, onClick = {}, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index d0967c3046..87f1b076b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -208,7 +209,7 @@ private fun TopBar( ) { RoundedActionButton( config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_sort_by_balance), + text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance), iconResId = R.drawable.ic_sort_24, onClick = config.onSortByBalanceClick, ), @@ -217,7 +218,7 @@ private fun TopBar( ) RoundedActionButton( config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_group), + text = TextReference.Res(id = R.string.organize_tokens_group), iconResId = R.drawable.ic_group_24, onClick = config.onGroupByNetworkClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 642d59b17b..de6989f860 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -90,6 +90,10 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } + override fun openTxHistoryWebsite(url: String) { + navigationStateHolder.navigate(action = NavigationAction.OpenUrl(url)) + } + private companion object { const val BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN = 2 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index ef0a6a93b3..5fab6367de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -37,4 +37,7 @@ internal interface InnerWalletRouter : WalletRouter { /** Open onboarding screen */ fun openOnboardingScreen() + + /** Open transaction history website by [url] */ + fun openTxHistoryWebsite(url: String) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt index e5f6115aaa..cb07819add 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R /** @@ -19,7 +20,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Buy(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Buy", + text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, onClick = onClick, ), @@ -32,7 +33,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Send(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Send", + text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, ), @@ -45,7 +46,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Receive(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Receive", + text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, ), @@ -58,7 +59,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Exchange(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Exchange", + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, ), @@ -71,7 +72,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class CopyAddress(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Copy address", + text = TextReference.Res(id = R.string.common_copy_address), iconResId = R.drawable.ic_copy_24, onClick = onClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt index f9b3c822ee..5252fa8981 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt @@ -17,14 +17,32 @@ internal sealed interface WalletTxHistoryState { * * @property items content items */ - sealed class ContentItemsState(open val items: Flow>) : WalletTxHistoryState + sealed class ContentState(open val items: Flow>) : WalletTxHistoryState /** * Content state * * @property items content items */ - data class Content(override val items: Flow>) : ContentItemsState(items) + data class Content(override val items: Flow>) : ContentState(items) + + /** + * Locked state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Locked(val onExploreClick: () -> Unit) : + ContentState( + items = flowOf( + PagingData.from( + listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction(state = TransactionState.Loading), + ), + ), + ), + ), + WalletLockedContentState /** * Empty state @@ -47,24 +65,6 @@ internal sealed interface WalletTxHistoryState { */ data class Error(val onReloadClick: () -> Unit) : WalletTxHistoryState - /** - * Locked state - * - * @property onExploreClick lambda be invoke when explore button was clicked - */ - data class Locked(val onExploreClick: () -> Unit) : - ContentItemsState( - items = flowOf( - PagingData.from( - listOf( - TxHistoryItemState.Title(onExploreClick = onExploreClick), - TxHistoryItemState.Transaction(state = TransactionState.Loading), - ), - ), - ), - ), - WalletLockedContentState - /** Transactions history item state */ sealed interface TxHistoryItemState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 1ba7a134d6..05f45bd9e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -19,22 +19,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemsIndexed import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.MultiCurrencyContentItem import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.OrganizeTokensButton -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.SingleCurrencyContentItem -import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.txHistoryItems import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -69,7 +66,7 @@ internal fun WalletScreen(state: WalletStateHolder) { .pullRefresh(pullRefreshState), ) { val txHistoryItems = if (state is WalletStateHolder.SingleCurrencyContent) { - if (state.txHistoryState is WalletTxHistoryState.ContentItemsState) { + if (state.txHistoryState is WalletTxHistoryState.ContentState) { state.txHistoryState.items.collectAsLazyPagingItems() } else { null @@ -177,52 +174,6 @@ private fun LazyListScope.contentItems( } } -private fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { - itemsIndexed( - items = state.items, - key = { index, _ -> index }, - itemContent = { index, item -> - MultiCurrencyContentItem( - state = item, - modifier = modifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = state.items.lastIndex, - ), - ) - }, - ) -} - -private fun LazyListScope.txHistoryItems( - state: WalletTxHistoryState, - txHistoryItems: LazyPagingItems?, - modifier: Modifier = Modifier, -) { - when (state) { - is WalletTxHistoryState.ContentItemsState -> { - checkNotNull(txHistoryItems) - itemsIndexed( - items = txHistoryItems, - key = { index, _ -> index }, - itemContent = { index, item -> - if (item == null) return@itemsIndexed - - SingleCurrencyContentItem( - state = item, - modifier = modifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = txHistoryItems.itemCount, - ), - ) - }, - ) - } - is WalletTxHistoryState.Empty -> TODO("[REDACTED_JIRA]") - is WalletTxHistoryState.Error -> TODO("[REDACTED_JIRA]") - is WalletTxHistoryState.NotSupported -> TODO("[REDACTED_JIRA]") - } -} - @OptIn(ExperimentalMaterialApi::class) @Composable private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt new file mode 100644 index 0000000000..c8fc7782f3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration + +/** + * LazyList extension for [WalletTokensListState] + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { + itemsIndexed( + items = state.items, + key = { index, _ -> index }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt index f7e77273fa..53aeef026b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R /** @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.impl.R internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { RoundedActionButton( config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_title), + text = TextReference.Res(id = R.string.organize_tokens_title), iconResId = R.drawable.ic_filter_24, onClick = onClick ?: {}, enabled = onClick != null, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt new file mode 100644 index 0000000000..3684c90d62 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemsIndexed +import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock +import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration + +/** + * LazyList extension for transactions history [WalletTxHistoryState] + * + * @param state state + * @param txHistoryItems transactions + * @param modifier modifier + */ +internal fun LazyListScope.txHistoryItems( + state: WalletTxHistoryState, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletTxHistoryState.ContentState -> { + contentItems( + txHistoryItems = requireNotNull(txHistoryItems), + modifier = modifier, + ) + } + is WalletTxHistoryState.Empty -> { + nonContentItem( + state = EmptyTransactionsBlockState.Empty(onClick = state.onBuyClick), + modifier = modifier, + ) + } + is WalletTxHistoryState.Error -> { + nonContentItem( + state = EmptyTransactionsBlockState.FailedToLoad(onClick = state.onReloadClick), + modifier = modifier, + ) + } + is WalletTxHistoryState.NotSupported -> { + nonContentItem( + state = EmptyTransactionsBlockState.NotImplemented(onClick = state.onExploreClick), + modifier = modifier, + ) + } + } +} + +private fun LazyListScope.contentItems( + txHistoryItems: LazyPagingItems, + modifier: Modifier = Modifier, +) { + itemsIndexed( + items = txHistoryItems, + key = { index, _ -> index }, + itemContent = { index, item -> + if (item == null) return@itemsIndexed + + SingleCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = txHistoryItems.itemSnapshotList.lastIndex, + ), + ) + }, + ) +} + +private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { + item { + EmptyTransactionBlock( + state = state, + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index af2d2daddd..b0ee734fe4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import androidx.paging.cachedIn import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure @@ -125,22 +126,24 @@ internal class WalletViewModel @Inject constructor( private fun updateByTxHistory(index: Int) { viewModelScope.launch(dispatchers.io) { - val wallet = getWallet(index) - val blockchain = wallet.scanResponse.cardTypesResolver.getBlockchain() - uiState = stateFactory.getLoadingTxHistoryState( - itemsCountEither = txHistoryItemsCountUseCase( - networkId = blockchain.id, - derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath, - ), + val blockchain = getWallet(index).scanResponse.cardTypesResolver.getBlockchain() + + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = blockchain.id, + derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath, ) - updateTxHistory(networkId = blockchain.id) + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { updateTxHistory(networkId = blockchain.id) } updateNotifications(index) } } private fun updateTxHistory(networkId: String) { - uiState = stateFactory.getLoadedTxHistoryState(txHistoryEither = txHistoryItemsUseCase(networkId)) + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase(networkId = networkId).map { it.cachedIn(viewModelScope) }, + ) } private fun updateNotifications(index: Int, tokenList: TokenList? = null) { @@ -255,10 +258,11 @@ internal class WalletViewModel @Inject constructor( } override fun onReloadClick() { - // TODO: [REDACTED_JIRA] + uiState = stateFactory.getStateAfterContentRefreshing() + updateByTxHistory(index = uiState.walletsListConfig.selectedWalletIndex) } override fun onExploreClick() { - // TODO: [REDACTED_JIRA] + router.openTxHistoryWebsite(url = "") // TODO } } \ No newline at end of file From cd78832ff19422441ccf51297e701a079e2aa00a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Aug 2023 15:48:39 +0800 Subject: [PATCH 096/100] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 8 ++++++++ data/tokens/build.gradle.kts | 1 + .../tangem/data/tokens/mock/MockNetworks.kt | 2 +- .../repository/MockNetworksRepository.kt | 2 +- .../data/tokens/utils/TokensOperations.kt | 2 +- domain/legacy/build.gradle.kts | 1 + .../DefaultWalletManagersFacade.kt | 19 ++++++++++++++++++- .../walletmanager/WalletManagersFacade.kt | 4 +++- domain/tokens/build.gradle.kts | 1 + domain/tokens/models/.gitignore | 1 + domain/tokens/models/build.gradle.kts | 4 ++++ .../tangem/domain/tokens/models}/Network.kt | 7 ++----- .../tokens/ApplyTokenListSortingUseCase.kt | 2 +- .../tokens/ToggleTokenListGroupingUseCase.kt | 2 +- .../domain/tokens/model/CryptoCurrency.kt | 2 ++ .../domain/tokens/model/NetworkGroup.kt | 2 ++ .../domain/tokens/model/NetworkStatus.kt | 1 + .../CurrenciesStatusesOperations.kt | 1 + .../tokens/operations/TokenListOperations.kt | 2 +- .../operations/TokenListSortingOperations.kt | 2 +- .../tokens/repository/NetworksRepository.kt | 2 +- .../domain/tokens/GetTokenListUseCaseTest.kt | 2 +- .../tokens/ToggleTokenListGroupingTest.kt | 2 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 2 +- .../repository/MockNetworksRepository.kt | 2 +- domain/wallets/build.gradle.kts | 7 ++++--- .../wallets/usecase/GetExploreUrlUseCase.kt | 12 ++++++++++++ features/wallet/impl/build.gradle.kts | 1 + .../wallet/viewmodels/WalletViewModel.kt | 15 ++++++++++++++- settings.gradle.kts | 1 + 30 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 domain/tokens/models/.gitignore create mode 100644 domain/tokens/models/build.gradle.kts rename domain/tokens/{src/main/kotlin/com/tangem/domain/tokens/model => models/src/main/java/com/tangem/domain/tokens/models}/Network.kt (88%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 6fedc25c0e..9321638b94 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,6 +1,8 @@ package com.tangem.tap.di.domain +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import dagger.Module @@ -24,4 +26,10 @@ internal object WalletsDomainModule { fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) } + + @Provides + @ViewModelScoped + fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { + return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) + } } \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 3a9ee305bf..5b3c4c0e7a 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) implementation(projects.domain.demo) implementation(projects.domain.wallets.models) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt index 12256c2120..8278cce392 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt @@ -1,7 +1,7 @@ package com.tangem.data.tokens.mock -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt index c6919ea86b..3455d66764 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.tokens.repository import com.tangem.data.tokens.mock.MockNetworks import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 627f30111d..2d0b915285 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -7,7 +7,7 @@ import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.models.Network import com.tangem.blockchain.common.Token as SdkToken private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index a649d1ced1..99665896d3 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) /** Tangem libraries */ diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index dabbd65f90..09bc5ac1dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -11,7 +11,7 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.SdkTokenConverter import com.tangem.domain.walletmanager.utils.UpdateWalletManagerResultFactory @@ -46,6 +46,23 @@ class DefaultWalletManagersFacade( return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) } + override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + + val blockchain = Blockchain.fromId(networkId.value) + + return getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath(userWallet.scanResponse.card.derivationStyle), + ) + ?.wallet + ?.getExploreUrl() + .orEmpty() + } + private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, blockchain: Blockchain, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4c4fd8d26d..a2ed074675 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -1,7 +1,7 @@ package com.tangem.domain.walletmanager import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId @@ -24,4 +24,6 @@ interface WalletManagersFacade { networkId: Network.ID, extraTokens: Set, ): UpdateWalletManagerResult + + suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index d6c92daddb..4099f104eb 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { implementation(projects.domain.core) + implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.core.utils) diff --git a/domain/tokens/models/.gitignore b/domain/tokens/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/tokens/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/tokens/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt similarity index 88% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index 402ae364eb..48ba69226f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.model +package com.tangem.domain.tokens.models /** * Represents a blockchain network, identified by a unique ID and a human-readable name. @@ -8,10 +8,7 @@ package com.tangem.domain.tokens.model * * @throws IllegalArgumentException If the name or ID is blank. */ -data class Network( - val id: ID, - val name: String, -) { +data class Network(val id: ID, val name: String) { init { require(name.isNotBlank()) { "Network name must not be blank" } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index c1bad61702..89d9776a18 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -8,7 +8,7 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index cb53f81ec2..fe70f6aacd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -7,8 +7,8 @@ import arrow.core.raise.either import arrow.core.raise.ensure import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt index 504e7ef421..5143f90497 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -1,5 +1,7 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.Network + /** * Represents a generic cryptocurrency. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index b244d7252f..89264ac332 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -1,5 +1,7 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.Network + /** * Represents a group of cryptocurrencies associated with a specific network. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index 454480611c..17bbaa392f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.Network import java.math.BigDecimal /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 41b8fbae2e..6ce5fc8b60 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index a39720b230..37b96a781c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -8,8 +8,8 @@ import arrow.core.toNonEmptySetOrNull import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index debb451f54..06c3041651 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -7,9 +7,9 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index dfb3a25ef1..5acf293b11 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 57101b51f6..204bf4af9c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -10,9 +10,9 @@ import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokenLists import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository import com.tangem.domain.tokens.repository.MockTokensRepository diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt index 0faaff77ea..8e34224394 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt @@ -7,7 +7,7 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index a2194499b2..4bf559f4c7 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -2,8 +2,8 @@ package com.tangem.domain.tokens.mock import arrow.core.NonEmptySet import arrow.core.nonEmptySetOf -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 649cd7808c..ced27621b7 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -4,8 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 31807a9a48..3216145e52 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -11,9 +11,10 @@ android { dependencies { // region Domain modules - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:wallets:models")) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) // endregion // region Tangem libraries diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt new file mode 100644 index 0000000000..564192c4f8 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFacade) { + + suspend operator fun invoke(userWalletId: UserWalletId, networkId: Network.ID): String { + return walletsManagersFacade.getExploreUrl(userWalletId, networkId) + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 606d847a63..265fb7ce41 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index b0ee734fe4..4c1275a2bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -16,10 +16,12 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -52,6 +54,7 @@ internal class WalletViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -263,6 +266,16 @@ internal class WalletViewModel @Inject constructor( } override fun onExploreClick() { - router.openTxHistoryWebsite(url = "") // TODO + viewModelScope.launch(dispatchers.io) { + val wallet = getWallet(uiState.walletsListConfig.selectedWalletIndex) + router.openTxHistoryWebsite( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + networkId = Network.ID( + value = wallet.scanResponse.cardTypesResolver.getBlockchain().id, + ), + ), + ) + } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 18308d4085..7378b7b744 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -103,6 +103,7 @@ include(":domain:core") include(":domain:demo") include(":domain:settings") include(":domain:tokens") +include(":domain:tokens:models") include(":domain:wallets") include(":domain:wallets:models") include(":domain:txhistory") From c4fcd17befe9cf0422ee0ebd9dd817ec22f1cebd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Aug 2023 16:28:14 +0300 Subject: [PATCH 097/100] Updated on 2026-08-14 --- .../warningMessage/WarningMessagesManager.kt | 6 +++--- .../com/tangem/utils/extensions/Collection.kt | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 183ba76207..01bb883f69 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.configurable.warningMessage import com.tangem.blockchain.common.Blockchain -import com.tangem.utils.extensions.removeBy +import com.tangem.utils.extensions.removeByReplace import com.tangem.wallet.R import java.util.concurrent.CopyOnWriteArrayList @@ -44,12 +44,12 @@ class WarningMessagesManager { } fun removeWarnings(origin: WarningMessage.Origin) { - warningsList.removeBy { it.origin == origin } + warningsList.removeByReplace { it.origin == origin } sortByPriority() } fun removeWarnings(messageRes: Int) { - warningsList.removeBy { it.messageResId == messageRes } + warningsList.removeByReplace { it.messageResId == messageRes } } fun containsWarning(warning: WarningMessage) = warning in warningsList diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 36ef7e1ad6..36644cfda9 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -53,11 +53,12 @@ inline fun MutableCollection.addOrReplace(item: T, predicate: (T) -> Bool /** * Removes an element from the collection based on the provided predicate. + * Uses iterator, avoid using it in COW collections * * @param predicate The condition to remove an element. * @return [Boolean] indicating whether an element was removed. */ -inline fun MutableCollection.removeBy(predicate: (T) -> Boolean): Boolean { +inline fun MutableCollection.removeByIterate(predicate: (T) -> Boolean): Boolean { var removed = false val iterator = this.iterator() @@ -73,6 +74,19 @@ inline fun MutableCollection.removeBy(predicate: (T) -> Boolean): Boolean return removed } +/** + * Removes an element from the collection based on the provided predicate. + * Uses removeAll() method and could be used for COW collections + * + * @param predicate The condition to remove an element. + * @return [Boolean] indicating whether an element was removed. + */ +fun MutableList.removeByReplace(predicate: (T) -> Boolean): Boolean { + val toRemove = this.filter(predicate) + this.removeAll(toRemove) + return toRemove.isNotEmpty() +} + /** * Replaces an element in the collection with the provided item based on the predicate. * From efe855e431e8745e83927c14ba305493dc158699 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Aug 2023 16:48:00 +0300 Subject: [PATCH 098/100] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/home/redux/HomeMiddleware.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 863083a6e0..71f724d931 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -19,7 +20,6 @@ import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.signin.redux.SignInAction From e2b8b4b660ef402729d23e4cbb9e3186f8d614a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Aug 2023 14:13:33 +0300 Subject: [PATCH 099/100] Updated on 2026-08-14 --- .../tokens/impl/presentation/states/TokenItemState.kt | 7 +++++++ .../tokens/impl/presentation/ui/TokenListPreviewData.kt | 2 ++ .../tokens/impl/presentation/ui/TokensListScreen.kt | 2 +- .../impl/presentation/viewmodels/TokensListViewModel.kt | 4 ++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt index 790a83fb5f..a5f9c615fd 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokenItemState.kt @@ -25,6 +25,9 @@ sealed interface TokenItemState { /** List of networks */ val networks: ImmutableList + /** Token composed id that unique for tokens in different networks */ + val composedId: String + /** * Token item state that is available for read * @@ -32,12 +35,14 @@ sealed interface TokenItemState { * @property fullName token name * @property iconUrl token icon url * @property networks list of networks that is available for read + * @property composedId token composed id to use in lists */ data class ReadContent( override val id: String, override val fullName: String, override val iconUrl: String, override val networks: ImmutableList, + override val composedId: String, ) : TokenItemState /** @@ -47,6 +52,7 @@ sealed interface TokenItemState { * @property fullName token name * @property iconUrl token icon url * @property networks list of networks is available for read and edit + * @property composedId token composed id to use in lists * @property name token name * @property symbol token brief name */ @@ -55,6 +61,7 @@ sealed interface TokenItemState { override val fullName: String, override val iconUrl: String, override val networks: ImmutableList, + override val composedId: String, val name: String, val symbol: String, ) : TokenItemState diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt index addc7020c9..819e5e4fb7 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt @@ -18,6 +18,7 @@ object TokenListPreviewData { fullName = "Tether (USDT)", iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", networks = createManageNetworksList(), + composedId = "11231", id = "", name = "Tether", symbol = "", @@ -30,6 +31,7 @@ object TokenListPreviewData { fullName = "Tether (USDT)", iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", networks = createReadNetworksList(), + composedId = "11231", ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 6896480058..b35bcd93bc 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -134,7 +134,7 @@ private fun TokensListContent( item { DifferentAddressesWarning() } } - items(items = tokens, key = TokenItemState::id) { + items(items = tokens, key = TokenItemState::composedId) { it?.let { TokenItem(model = it) } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 93d386871e..c9ab947187 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -131,10 +131,12 @@ internal class TokensListViewModel @Inject constructor( } private fun createManageTokenContent(token: Token): TokenItemState.ManageContent { + val currentTime = System.nanoTime() return TokenItemState.ManageContent( fullName = getTokenFullName(token), iconUrl = token.iconUrl, networks = token.networks.map(::createManageNetworkContent).toImmutableList(), + composedId = token.id + currentTime, id = token.id, name = token.name, symbol = token.symbol, @@ -162,11 +164,13 @@ internal class TokensListViewModel @Inject constructor( } private fun createReadTokenContent(token: Token): TokenItemState.ReadContent { + val currentTime = System.nanoTime() return TokenItemState.ReadContent( id = token.id, fullName = getTokenFullName(token), iconUrl = token.iconUrl, networks = token.networks.map(::createReadNetworkContent).toImmutableList(), + composedId = token.id + currentTime, ) } From eaa1d4111fb4fde328a0777fa11e8261a4ffc515 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Aug 2023 17:56:02 +0400 Subject: [PATCH 100/100] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TapApplication.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index b4783eda19..cb21e52c1d 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -10,7 +10,6 @@ import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.blockchain.common.BlockchainSdkConfig -import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics @@ -285,7 +284,7 @@ class TapApplication : Application(), ImageLoaderFactory { jsonConverter = MoshiConverter.sdkMoshiConverter, ) factory.build(Analytics, buildData) - ExceptionHandler.append(blockchainExceptionHandler) + // ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA] } private fun initFeedbackManager(